package rfc3164

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/crowdsecurity/go-cs-lib/cstest"
)

func TestPri(t *testing.T) {
	tests := []struct {
		input       string
		expected    int
		expectedErr string
	}{
		{"<0>", 0, ""},
		{"<19>", 19, ""},
		{"<200>", 200, ""},
		{"<4999>", 0, "PRI must be up to 3 characters long"},
		{"<123", 0, "PRI must end with '>'"},
		{"123>", 0, "PRI must start with '<'"},
		{"<abc>", 0, "PRI must be a number"},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			r := &RFC3164{}
			r.buf = []byte(test.input)
			r.len = len(r.buf)

			err := r.parsePRI()
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected, r.PRI)
		})
	}
}

func TestTimestamp(t *testing.T) {
	tests := []struct {
		input       string
		expected    string
		expectedErr string
		currentYear bool
	}{
		{"May 20 09:33:54", "0000-05-20T09:33:54Z", "", false},
		{"May 20 09:33:54", fmt.Sprintf("%d-05-20T09:33:54Z", time.Now().Year()), "", true},
		{"May 20 09:33:54 2022", "2022-05-20T09:33:54Z", "", false},
		{"May  1 09:33:54 2022", "2022-05-01T09:33:54Z", "", false},
		{"May 01 09:33:54 2021", "2021-05-01T09:33:54Z", "", true},
		{"foobar", "", "timestamp is not valid", false},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			opts := []RFC3164Option{}
			if test.currentYear {
				opts = append(opts, WithCurrentYear())
			}

			r := NewRFC3164Parser(opts...)
			r.buf = []byte(test.input)
			r.len = len(r.buf)

			err := r.parseTimestamp()
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected, r.Timestamp.Format(time.RFC3339))
		})
	}
}

func TestHostname(t *testing.T) {
	tests := []struct {
		input          string
		expected       string
		expectedErr    string
		strictHostname bool
	}{
		{"127.0.0.1", "127.0.0.1", "", false},
		{"::1", "::1", "", false},
		{"foo.-bar", "", "hostname is not valid", true},
		{"foo-.bar", "", "hostname is not valid", true},
		{"foo123.bar", "foo123.bar", "", true},
		{"a..", "", "hostname is not valid", true},
		{"foo.bar", "foo.bar", "", false},
		{"foo,bar", "foo,bar", "", false},
		{"foo,bar", "", "hostname is not valid", true},
		{"", "", "hostname is empty", false},
		{".", ".", "", true},
		{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "", "hostname is not valid", true},
		{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bla", "", "hostname is not valid", true},
		{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bla", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bla", "", false},
		{"a.foo-", "", "hostname is not valid", true},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			opts := []RFC3164Option{}
			if test.strictHostname {
				opts = append(opts, WithStrictHostname())
			}

			r := NewRFC3164Parser(opts...)
			r.buf = []byte(test.input)
			r.len = len(r.buf)

			err := r.parseHostname()
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected, r.Hostname)
		})
	}
}

func TestTag(t *testing.T) {
	tests := []struct {
		input       string
		expected    string
		expectedPID string
		expectedErr string
	}{
		{"foobar", "foobar", "", ""},
		{"foobar[42]", "foobar", "42", ""},
		{"", "", "", "tag is empty"},
		{"foobar[", "", "", "pid inside tag must be closed with ']'"},
		{"foobar[42", "", "", "pid inside tag must be closed with ']'"},
		{"foobar[asd]", "foobar", "", "pid inside tag must be a number"},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			r := &RFC3164{}
			r.buf = []byte(test.input)
			r.len = len(r.buf)

			err := r.parseTag()
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected, r.Tag)
			assert.Equal(t, test.expectedPID, r.PID)
		})
	}
}

func TestMessage(t *testing.T) {
	tests := []struct {
		input       string
		expected    string
		expectedErr string
	}{
		{"foobar: pouet", "pouet", ""},
		{"foobar[42]: test", "test", ""},
		{"foobar[123]: this is a test", "this is a test", ""},
		{"foobar[123]: ", "", "message is empty"},
		{"foobar[123]:", "", "message is empty"},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			r := &RFC3164{}
			r.buf = []byte(test.input)
			r.len = len(r.buf)

			err := r.parseMessage()
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected, r.Message)
		})
	}
}

func TestParse(t *testing.T) {
	type expected struct {
		Timestamp time.Time
		Hostname  string
		Tag       string
		PID       string
		Message   string
		PRI       int
	}

	tests := []struct {
		input       string
		expected    expected
		expectedErr string
		opts        []RFC3164Option
	}{
		{
			"<12>May 20 09:33:54 UDMPRO,a2edd0c6ae48,udm-1.10.0.3686 kernel: foo", expected{
				Timestamp: time.Date(0, time.May, 20, 9, 33, 54, 0, time.UTC),
				Hostname:  "UDMPRO,a2edd0c6ae48,udm-1.10.0.3686",
				Tag:       "kernel",
				PID:       "",
				Message:   "foo",
				PRI:       12,
			}, "", []RFC3164Option{},
		},
		{
			"<12>May 20 09:33:54 UDMPRO,a2edd0c6ae48,udm-1.10.0.3686 kernel: foo", expected{
				Timestamp: time.Date(time.Now().Year(), time.May, 20, 9, 33, 54, 0, time.UTC),
				Hostname:  "UDMPRO,a2edd0c6ae48,udm-1.10.0.3686",
				Tag:       "kernel",
				PID:       "",
				Message:   "foo",
				PRI:       12,
			}, "", []RFC3164Option{WithCurrentYear()},
		},
		{
			"<12>May 20 09:33:54 UDMPRO,a2edd0c6ae48,udm-1.10.0.3686 kernel: foo", expected{}, "hostname is not valid", []RFC3164Option{WithStrictHostname()},
		},
		{
			"foobar", expected{}, "PRI must start with '<'", []RFC3164Option{},
		},
		{
			"<12>", expected{}, "timestamp is not valid", []RFC3164Option{},
		},
		{
			"<12 May 02 09:33:54 foo.bar", expected{}, "PRI must be a number", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54", expected{}, "hostname is empty", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar", expected{}, "tag is empty", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar bla[42", expected{}, "pid inside tag must be closed with ']'", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar bla[42]", expected{}, "message is empty", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar bla[42]:   ", expected{}, "message is empty", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar bla", expected{}, "message is empty", []RFC3164Option{},
		},
		{
			"<12>May 02 09:33:54 foo.bar bla:", expected{}, "message is empty", []RFC3164Option{},
		},
		{
			"", expected{}, "message is empty", []RFC3164Option{},
		},
		{
			`<13>1 2021-05-18T11:58:40.828081+02:00 mantis sshd 49340 - [timeQuality isSynced="0" tzKnown="1"] blabla`, expected{}, "timestamp is not valid", []RFC3164Option{},
		},
		{
			`<46>Jun  2 06:55:39 localhost haproxy[27213]: Connect from 100.100.100.99:52611 to 100.100.100.99:443 (https_shared-merged/HTTP)\\n 10.0.0.1}`, expected{
				Timestamp: time.Date(time.Now().Year(), time.June, 2, 6, 55, 39, 0, time.UTC),
				Hostname:  "localhost",
				Tag:       "haproxy",
				PID:       "27213",
				Message:   `Connect from 100.100.100.99:52611 to 100.100.100.99:443 (https_shared-merged/HTTP)\\n 10.0.0.1}`,
				PRI:       46,
			}, "", []RFC3164Option{WithCurrentYear()},
		},
		{
			`<46>Jun  2 06:55:39 2022 localhost haproxy[27213]: Connect from 100.100.100.99:52611 to 100.100.100.99:443 (https_shared-merged/HTTP)\\n 10.0.0.1}`, expected{
				Timestamp: time.Date(2022, time.June, 2, 6, 55, 39, 0, time.UTC),
				Hostname:  "localhost",
				Tag:       "haproxy",
				PID:       "27213",
				Message:   `Connect from 100.100.100.99:52611 to 100.100.100.99:443 (https_shared-merged/HTTP)\\n 10.0.0.1}`,
				PRI:       46,
			}, "", []RFC3164Option{},
		},
	}

	for _, test := range tests {
		t.Run(test.input, func(t *testing.T) {
			r := NewRFC3164Parser(test.opts...)

			err := r.Parse([]byte(test.input))
			cstest.RequireErrorContains(t, err, test.expectedErr)

			if test.expectedErr != "" {
				return
			}

			assert.Equal(t, test.expected.Timestamp, r.Timestamp)
			assert.Equal(t, test.expected.Hostname, r.Hostname)
			assert.Equal(t, test.expected.Tag, r.Tag)
			assert.Equal(t, test.expected.PID, r.PID)
			assert.Equal(t, test.expected.Message, r.Message)
			assert.Equal(t, test.expected.PRI, r.PRI)
		})
	}
}
