package rfc3164

import (
	"testing"
	"time"

	"github.com/leodido/go-syslog/v4"
	"github.com/leodido/go-syslog/v4/ciscoios"
	syslogtesting "github.com/leodido/go-syslog/v4/testing"
	"github.com/stretchr/testify/assert"
)

// todo > add support for testing `best effort` mode

type testCase struct {
	opts         []syslog.MachineOption
	input        []byte
	valid        bool
	value        syslog.Message
	errorString  string
	partialValue syslog.Message
}

var testCases = []testCase{
	{
		input: []byte(`<34>Jan 12 06:30:00 xxx apache: 1.2.3.4 - - [12/Jan/2011:06:29:59 +0100] "GET /foo/bar.html HTTP/1.1" 301 96 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729)" PID 18904 Time Taken 0`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(34),
				Severity:  syslogtesting.Uint8Address(2),
				Facility:  syslogtesting.Uint8Address(4),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Jan 12 06:30:00"),
				Hostname:  syslogtesting.StringAddress("xxx"),
				Appname:   syslogtesting.StringAddress("apache"),
				Message:   syslogtesting.StringAddress(`1.2.3.4 - - [12/Jan/2011:06:29:59 +0100] "GET /foo/bar.html HTTP/1.1" 301 96 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729)" PID 18904 Time Taken 0`),
			},
		},
	},
	{
		input: []byte(`<34>Aug  7 06:30:00 xxx aaa: message from 1.2.3.4`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(34),
				Severity:  syslogtesting.Uint8Address(2),
				Facility:  syslogtesting.Uint8Address(4),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Aug 7 06:30:00"),
				Hostname:  syslogtesting.StringAddress("xxx"),
				Appname:   syslogtesting.StringAddress("aaa"),
				Message:   syslogtesting.StringAddress(`message from 1.2.3.4`),
			},
		},
	},
	{
		input: []byte(`<85>Jan 24 15:50:41 ip-172-31-30-110 sudo[6040]: ec2-user : TTY=pts/0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/tail secure`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(85),
				Facility:  syslogtesting.Uint8Address(10),
				Severity:  syslogtesting.Uint8Address(5),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Jan 24 15:50:41"),
				Hostname:  syslogtesting.StringAddress("ip-172-31-30-110"),
				Appname:   syslogtesting.StringAddress("sudo"),
				ProcID:    syslogtesting.StringAddress("6040"),
				Message:   syslogtesting.StringAddress(`ec2-user : TTY=pts/0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/tail secure`),
			},
		},
	},
	{
		input: []byte(`<166>Jul  6 20:33:28 ABC-1-234567 Some message here`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(166),
				Facility:  syslogtesting.Uint8Address(20),
				Severity:  syslogtesting.Uint8Address(6),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Jul  6 20:33:28"),
				Hostname:  syslogtesting.StringAddress("ABC-1-234567"),
				Message:   syslogtesting.StringAddress("Some message here"),
			},
		},
	},
	{
		input: []byte(`<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(34),
				Facility:  syslogtesting.Uint8Address(4),
				Severity:  syslogtesting.Uint8Address(2),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Oct 11 22:14:15"),
				Hostname:  syslogtesting.StringAddress("mymachine"),
				Appname:   syslogtesting.StringAddress("su"),
				Message:   syslogtesting.StringAddress("'su root' failed for lonvick on /dev/pts/8"),
			},
		},
	},
	{
		input: []byte(`<13>Feb  5 17:32:18 10.0.0.99 Use the BFG!`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(13),
				Facility:  syslogtesting.Uint8Address(1),
				Severity:  syslogtesting.Uint8Address(5),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Feb  5 17:32:18"),
				Hostname:  syslogtesting.StringAddress("10.0.0.99"),
				Message:   syslogtesting.StringAddress("Use the BFG!"),
			},
		},
	},
	// Zero-prefixed day rejected without WithLenientDay
	{
		input:       []byte(`<13>Feb 05 17:32:18 10.0.0.99 Use the BFG!`),
		valid:       false,
		errorString: "expecting a Stamp timestamp [col 8]",
		partialValue: &SyslogMessage{
			Base: syslog.Base{
				Priority: syslogtesting.Uint8Address(13),
				Facility: syslogtesting.Uint8Address(1),
				Severity: syslogtesting.Uint8Address(5),
			},
		},
	},
	// Zero-prefixed day accepted with WithLenientDay
	{
		opts:  []syslog.MachineOption{WithLenientDay()},
		input: []byte(`<13>Feb 05 17:32:18 10.0.0.99 Use the BFG!`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(13),
				Facility:  syslogtesting.Uint8Address(1),
				Severity:  syslogtesting.Uint8Address(5),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Feb  5 17:32:18"),
				Hostname:  syslogtesting.StringAddress("10.0.0.99"),
				Message:   syslogtesting.StringAddress("Use the BFG!"),
			},
		},
	},

	{
		input: []byte(`<165>Aug 24 05:34:00 mymachine myproc[10]: %% It's time to make the do-nuts.  %%  Ingredients: Mix=OK, Jelly=OK # Devices: Mixer=OK, Jelly_Injector=OK, Frier=OK # Transport: Conveyer1=OK, Conveyer2=OK # %%`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(165),
				Facility:  syslogtesting.Uint8Address(20),
				Severity:  syslogtesting.Uint8Address(5),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Aug 24 05:34:00"),
				Hostname:  syslogtesting.StringAddress("mymachine"),
				Appname:   syslogtesting.StringAddress("myproc"),
				ProcID:    syslogtesting.StringAddress("10"),
				Message:   syslogtesting.StringAddress(`%% It's time to make the do-nuts.  %%  Ingredients: Mix=OK, Jelly=OK # Devices: Mixer=OK, Jelly_Injector=OK, Frier=OK # Transport: Conveyer1=OK, Conveyer2=OK # %%`),
			},
		},
	},
	{
		input: []byte(`<0>Oct 22 10:52:01 10.1.2.3 sched[0]: That's All Folks!`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(0),
				Facility:  syslogtesting.Uint8Address(0),
				Severity:  syslogtesting.Uint8Address(0),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Oct 22 10:52:01"),
				Hostname:  syslogtesting.StringAddress("10.1.2.3"),
				Appname:   syslogtesting.StringAddress("sched"),
				ProcID:    syslogtesting.StringAddress("0"),
				Message:   syslogtesting.StringAddress(`That's All Folks!`),
			},
		},
	},
	{
		input: []byte(`<0>Sep 17 04:05:37 host.domain.tld monit[2340:0]: subsvc[12345]: Message with [square brackets]`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(0),
				Facility:  syslogtesting.Uint8Address(0),
				Severity:  syslogtesting.Uint8Address(0),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Sep 17 04:05:37"),
				Hostname:  syslogtesting.StringAddress("host.domain.tld"),
				Appname:   syslogtesting.StringAddress("monit"),
				ProcID:    syslogtesting.StringAddress("2340:0"),
				Message:   syslogtesting.StringAddress(`subsvc[12345]: Message with [square brackets]`),
			},
		},
	},
	{
		input: []byte(`<132> Apr  4 19:32:33 ix2105  IP6[015]: Beyond scope for packet`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(132),
				Facility:  syslogtesting.Uint8Address(16),
				Severity:  syslogtesting.Uint8Address(4),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Apr 4 19:32:33"),
				Hostname:  syslogtesting.StringAddress("ix2105"),
				Appname:   syslogtesting.StringAddress("IP6"),
				ProcID:    syslogtesting.StringAddress("015"),
				Message:   syslogtesting.StringAddress(`Beyond scope for packet`),
			},
		},
	},
	{
		// OpenBSD 7.5 syslog start
		input: []byte(`<46>Apr 27 23:06:01 syslogd[68529]: start`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(46),
				Facility:  syslogtesting.Uint8Address(5),
				Severity:  syslogtesting.Uint8Address(6),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Apr 27 23:06:01"),
				Appname:   syslogtesting.StringAddress("syslogd"),
				ProcID:    syslogtesting.StringAddress("68529"),
				Message:   syslogtesting.StringAddress(`start`),
			},
		},
	},
	{
		// OpenBSD 7.5 logger from cli
		input: []byte(`<13>Apr 27 23:06:09 catap: some test from cli`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(13),
				Facility:  syslogtesting.Uint8Address(1),
				Severity:  syslogtesting.Uint8Address(5),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Apr 27 23:06:09"),
				Appname:   syslogtesting.StringAddress("catap"),
				Message:   syslogtesting.StringAddress(`some test from cli`),
			},
		},
	},
	{
		// OpenBSD 7.5 doas command
		input: []byte(`<86>Apr 27 23:06:11 doas: catap ran command rcctl restart syslogd as root from /home`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(86),
				Facility:  syslogtesting.Uint8Address(10),
				Severity:  syslogtesting.Uint8Address(6),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Apr 27 23:06:11"),
				Appname:   syslogtesting.StringAddress("doas"),
				Message:   syslogtesting.StringAddress(`catap ran command rcctl restart syslogd as root from /home`),
			},
		},
	},
	{
		input: []byte(`<30>Apr 29 11:44:07 cabinet b4fbe48b48ad,UAP-nanoHD-6.6.55+15189: stahtd: stahtd[17512]: [STA-TRACKER].stahtd_dump_event(): {"message_type":"STA_ASSOC_TRACKER","mac":"98:8d:46:21:2b:6d","vap":"rai1","event_type":"fixup","assoc_status":"0","event_id":"1","arp_reply_gw_seen":"yes","dns_resp_seen":"yes","avg_rssi":"-51"}`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(30),
				Facility:  syslogtesting.Uint8Address(3),
				Severity:  syslogtesting.Uint8Address(6),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Apr 29 11:44:07"),
				Hostname:  syslogtesting.StringAddress("cabinet"),
				Appname:   syslogtesting.StringAddress("b4fbe48b48ad,UAP-nanoHD-6.6.55+15189"),
				Message:   syslogtesting.StringAddress(`stahtd: stahtd[17512]: [STA-TRACKER].stahtd_dump_event(): {"message_type":"STA_ASSOC_TRACKER","mac":"98:8d:46:21:2b:6d","vap":"rai1","event_type":"fixup","assoc_status":"0","event_id":"1","arp_reply_gw_seen":"yes","dns_resp_seen":"yes","avg_rssi":"-51"}`),
			},
		},
	},
	{
		// Cisco IOS (without NTP sync)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>643: *Jan  8 19:46:03.295: %LINEPROTO-5-UPDOWN: Line protocol on Interface Loopback100, changed state to up`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(643),
				Timestamp:      syslogtesting.TimeParse(time.StampMilli, "Jan  8 19:46:03.295"),
				Appname:        syslogtesting.StringAddress("%LINEPROTO-5-UPDOWN"),
				Message:        syslogtesting.StringAddress(`Line protocol on Interface Loopback100, changed state to up`),
			},
		},
	},
	{
		// Cisco IOS (with NTP sync)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>643: Jan  8 19:46:03.295: %LINEPROTO-5-UPDOWN: Line protocol on Interface Loopback100, changed state to up`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(643),
				Timestamp:      syslogtesting.TimeParse(time.StampMilli, "Jan  8 19:46:03.295"),
				Appname:        syslogtesting.StringAddress("%LINEPROTO-5-UPDOWN"),
				Message:        syslogtesting.StringAddress(`Line protocol on Interface Loopback100, changed state to up`),
			},
		},
	},
	{
		// Cisco IOS (with message counter and sequence number)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>105: 000104: Mar 12 07:12:10: %SYS-5-CONFIG_I: Configured from console by console`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(105),
				Sequence:       syslogtesting.Uint32Address(104),
				Timestamp:      syslogtesting.TimeParse(time.Stamp, "Mar 12 07:12:10"),
				Appname:        syslogtesting.StringAddress("%SYS-5-CONFIG_I"),
				Message:        syslogtesting.StringAddress(`Configured from console by console`),
			},
		},
	},
	{
		// Cisco IOS (with message counter and sequence number both disabled)
		// Note that in this case the router still includes the `:` from the message counter
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>: Mar 12 07:10:10: %SYS-5-CONFIG_I: Configured from console by console`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(0),
				Timestamp:      syslogtesting.TimeParse(time.Stamp, "Mar 12 07:10:10"),
				Appname:        syslogtesting.StringAddress("%SYS-5-CONFIG_I"),
				Message:        syslogtesting.StringAddress(`Configured from console by console`),
			},
		},
	},
	{
		// Cisco IOS with hostname
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>269614: myhostname: Apr 11 10:02:08: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet7/0/34, changed state to up`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(269614),
				Hostname:       syslogtesting.StringAddress("myhostname"),
				Timestamp:      syslogtesting.TimeParse(time.Stamp, "Apr 11 10:02:08"),
				Appname:        syslogtesting.StringAddress("%LINEPROTO-5-UPDOWN"),
				Message:        syslogtesting.StringAddress(`Line protocol on Interface GigabitEthernet7/0/34, changed state to up`),
			},
		},
	},
	{
		// Cisco IOS log with message counter and sequence number, but all parsing disabled.
		// This demonstrates configuration mismatch: the device sends msgcount+sequence,
		// but parser expects plain RFC3164 format (no Cisco components).
		// Parser fails immediately at col 5 when it sees digits instead of timestamp.
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.DisableMessageCounter | ciscoios.DisableSequenceNumber | ciscoios.DisableHostname),
		},
		input:       []byte(`<189>269614: 000104: Apr 11 10:02:08: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet7/0/34, changed state to up`),
		valid:       false,
		errorString: "expecting a sequence number (from 1 to max 255 digits) [col 5]",
		partialValue: &SyslogMessage{
			Base: syslog.Base{
				Priority: syslogtesting.Uint8Address(189),
				Facility: syslogtesting.Uint8Address(23),
				Severity: syslogtesting.Uint8Address(5),
			},
		},
	},
	{
		// Cisco IOS log with message counter and sequence number, but with sequence and hostname parsing disabled (only message counter enabled).
		// This demonstrates configuration mismatch:
		// device sends msgcount+sequence, parser expects only msgcount.
		// Parser successfully parses msgcount (269614),
		// then fails at col 13 when it sees another number (000104) instead of timestamp.
		// Demonstrates the known limitation that parser cannot auto-detect components.
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.DisableSequenceNumber | ciscoios.DisableHostname),
		},
		input:       []byte(`<189>269614: 000104: Apr 11 10:02:08: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet7/0/34, changed state to up`),
		valid:       false,
		errorString: "expecting a sequence number (from 1 to max 255 digits) [col 13]",
		partialValue: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(269614),
			},
		},
	},
	{
		// Cisco IOS with hostname, but with hostname parsing disabled.
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.DisableHostname),
		},
		input:       []byte(`<189>269614: 000104: myhostname: Apr 11 10:02:08: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet7/0/34, changed state to up`),
		valid:       false,
		errorString: "expecting an hostname (from 1 to max 255 US-ASCII characters) [col 21]",
		partialValue: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(269614),
				Sequence:       syslogtesting.Uint32Address(104),
			},
		},
	},
	{
		// Cisco IOS - Message counter only, no service sequence (common remote logging case)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>80: *Jan  1 23:12:12.493: %SYS-5-CONFIG_I: Configured from console by console`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(80),
				Timestamp:      syslogtesting.TimeParse(time.StampMilli, "Jan  1 23:12:12.493"),
				Appname:        syslogtesting.StringAddress("%SYS-5-CONFIG_I"),
				Message:        syslogtesting.StringAddress(`Configured from console by console`),
			},
		},
	},
	{
		// Cisco IOS - Service sequence with message counter disabled (leading colon + sequence)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<189>: 000088: *Jan  1 23:14:48.135: %SYS-5-CONFIG_I: Configured from console by console`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(189),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(5),
				MessageCounter: syslogtesting.Uint32Address(0),
				Sequence:       syslogtesting.Uint32Address(88),
				Timestamp:      syslogtesting.TimeParse(time.StampMilli, "Jan  1 23:14:48.135"),
				Appname:        syslogtesting.StringAddress("%SYS-5-CONFIG_I"),
				Message:        syslogtesting.StringAddress(`Configured from console by console`),
			},
		},
	},
	{
		// Cisco IOS - LINK-3-UPDOWN message (severity 3, different facility use case)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<187>116: 000073: *Jan  2 00:18:03: %LINK-3-UPDOWN: Interface GigabitEthernet0/10, changed state to down`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(187),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(3),
				MessageCounter: syslogtesting.Uint32Address(116),
				Sequence:       syslogtesting.Uint32Address(73),
				Timestamp:      syslogtesting.TimeParse(time.Stamp, "Jan  2 00:18:03"),
				Appname:        syslogtesting.StringAddress("%LINK-3-UPDOWN"),
				Message:        syslogtesting.StringAddress(`Interface GigabitEthernet0/10, changed state to down`),
			},
		},
	},
	{
		// Cisco IOS - LOGGINGHOST_STARTSTOP message (severity 6, informational)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<190>56: *Jan  2 00:16:02: %SYS-6-LOGGINGHOST_STARTSTOP: Logging to host 10.0.0.10 port 514 started - reconnection`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(190),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(6),
				MessageCounter: syslogtesting.Uint32Address(56),
				Timestamp:      syslogtesting.TimeParse(time.Stamp, "Jan  2 00:16:02"),
				Appname:        syslogtesting.StringAddress("%SYS-6-LOGGINGHOST_STARTSTOP"),
				Message:        syslogtesting.StringAddress(`Logging to host 10.0.0.10 port 514 started - reconnection`),
			},
		},
	},
	{
		// Cisco IOS - Large message counter value (269614 from real capture)
		opts: []syslog.MachineOption{
			WithCiscoIOSComponents(ciscoios.All),
		},
		input: []byte(`<187>269614: 000191: *Jan  1 23:25:10.888: %LINK-3-UPDOWN: Interface GigabitEthernet0/1, changed state to down`),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:       syslogtesting.Uint8Address(187),
				Facility:       syslogtesting.Uint8Address(23),
				Severity:       syslogtesting.Uint8Address(3),
				MessageCounter: syslogtesting.Uint32Address(269614),
				Sequence:       syslogtesting.Uint32Address(191),
				Timestamp:      syslogtesting.TimeParse(time.StampMilli, "Jan  1 23:25:10.888"),
				Appname:        syslogtesting.StringAddress("%LINK-3-UPDOWN"),
				Message:        syslogtesting.StringAddress(`Interface GigabitEthernet0/1, changed state to down`),
			},
		},
	},
	{
		input: []byte("<34>Jan 12 06:30:00 xxx apache: \t1.2.3.4 - - [12/Jan/2011:06:29:59 +0100] \"GET /foo/bar.html HTTP/1.1\" 301 96 \"-\" \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729)\" PID 18904 Time Taken 0"),
		valid: true,
		value: &SyslogMessage{
			Base: syslog.Base{
				Priority:  syslogtesting.Uint8Address(34),
				Severity:  syslogtesting.Uint8Address(2),
				Facility:  syslogtesting.Uint8Address(4),
				Timestamp: syslogtesting.TimeParse(time.Stamp, "Jan 12 06:30:00"),
				Hostname:  syslogtesting.StringAddress("xxx"),
				Appname:   syslogtesting.StringAddress("apache"),
				Message:   syslogtesting.StringAddress("\t1.2.3.4 - - [12/Jan/2011:06:29:59 +0100] \"GET /foo/bar.html HTTP/1.1\" 301 96 \"-\" \"Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 ( .NET CLR 3.5.30729)\" PID 18904 Time Taken 0"),
			},
		},
	},
	// todo > other test cases pleaaaase
}

func TestMachineParse(t *testing.T) {
	for _, tc := range testCases {
		t.Run(syslogtesting.RightPad(string(tc.input), 50), func(t *testing.T) {
			t.Parallel()

			m := NewMachine(tc.opts...)
			message, merr := m.Parse(tc.input)
			m.WithBestEffort()
			partial, perr := m.Parse(tc.input)

			if !tc.valid {
				assert.Nil(t, message)
				assert.Error(t, merr)
				assert.EqualError(t, merr, tc.errorString)

				assert.Equal(t, tc.partialValue, partial)
				assert.EqualError(t, perr, tc.errorString)
			}
			if tc.valid {
				assert.NoError(t, merr)
				assert.NotEmpty(t, message)
				assert.Equal(t, message, partial)
				assert.Equal(t, merr, perr)
			}

			assert.Equal(t, tc.value, message)
		})
	}
}

func TestMachineHasBestEffort(t *testing.T) {
	m := NewMachine()
	assert.False(t, m.HasBestEffort())
	m.WithBestEffort()
	assert.True(t, m.HasBestEffort())
}

func TestMachineErr(t *testing.T) {
	m := NewMachine().(*machine)
	// No error before parsing
	assert.Nil(t, m.Err())
	// Error after invalid input
	_, err := m.Parse([]byte("not a syslog message"))
	assert.Error(t, err)
	assert.Error(t, m.Err())
}

func TestParserHasBestEffort(t *testing.T) {
	p := NewParser()
	assert.False(t, p.HasBestEffort())

	p2 := NewParser(WithBestEffort())
	assert.True(t, p2.HasBestEffort())
}

func TestParserParseInvalid(t *testing.T) {
	p := NewParser()
	msg, err := p.Parse([]byte("invalid"))
	assert.Nil(t, msg)
	assert.Error(t, err)
}

func TestParserParseInvalidBestEffort(t *testing.T) {
	p := NewParser(WithBestEffort())
	msg, err := p.Parse([]byte(`<13>not valid`))
	assert.NotNil(t, msg)
	assert.Error(t, err)
}

func TestWithMessageCounter(t *testing.T) {
	m := NewMachine(WithMessageCounter())
	msg, err := m.Parse([]byte(`<189>643: Jan  8 19:46:03 myhost %LINEPROTO-5-UPDOWN: Line protocol on Interface Loopback100, changed state to up`))
	assert.NotNil(t, msg)
	assert.NoError(t, err)
}

func TestWithSequenceNumber(t *testing.T) {
	m := NewMachine(WithMessageCounter(), WithSequenceNumber())
	msg, err := m.Parse([]byte(`<189>105: 000104: Mar 12 07:12:10 myhost %SYS-5-CONFIG_I: Configured from console by console`))
	assert.NotNil(t, msg)
	assert.NoError(t, err)
}

func TestWithCiscoHostname(t *testing.T) {
	m := NewMachine(WithMessageCounter(), WithCiscoHostname())
	msg, err := m.Parse([]byte(`<189>269614: hostname1: Apr 11 10:02:08 %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet7/0/34, changed state to up`))
	assert.NotNil(t, msg)
	assert.NoError(t, err)
}

func TestWithEmbeddedNewlines_LF(t *testing.T) {
	m := NewMachine(WithEmbeddedNewlines())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp: line1\nline2\nline3"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	assert.Equal(t, "line1\nline2\nline3", *sm.Message)
}

func TestWithEmbeddedNewlines_CR(t *testing.T) {
	m := NewMachine(WithEmbeddedNewlines())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp: line1\rline2\rline3"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	assert.Equal(t, "line1\rline2\rline3", *sm.Message)
}

func TestWithEmbeddedNewlines_CRLF(t *testing.T) {
	m := NewMachine(WithEmbeddedNewlines())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp: line1\r\nline2\r\nline3"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	assert.Equal(t, "line1\r\nline2\r\nline3", *sm.Message)
}

func TestWithEmbeddedNewlines_TrailingLFStripped(t *testing.T) {
	// The grammar's '\n'? at the end of main still strips a single trailing LF
	// when the LF is at the very end of input (EOF boundary).
	// With WithEmbeddedNewlines, mex greedily consumes the trailing \n,
	// so it becomes part of the message. This is expected — the transport
	// layer (octet-counting) handles stripping when needed.
	m := NewMachine(WithEmbeddedNewlines())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp: hello\n"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	// With WithEmbeddedNewlines, the trailing \n is consumed by mex as content
	assert.Equal(t, "hello\n", *sm.Message)
}

func TestWithoutEmbeddedNewlines_Truncates(t *testing.T) {
	// Without the option, newlines truncate the message (original behavior).
	m := NewMachine(WithBestEffort())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp: line1\nline2"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	assert.Equal(t, "line1", *sm.Message)
}

func TestWithEmbeddedNewlines_ContentBlock(t *testing.T) {
	// Newlines inside [...] content blocks should also be accepted.
	m := NewMachine(WithEmbeddedNewlines())
	msg, err := m.Parse([]byte("<134>Apr 28 11:53:44 myhost myapp[foo\nbar]: hello"))
	assert.NoError(t, err)
	assert.NotNil(t, msg)
	sm := msg.(*SyslogMessage)
	assert.Equal(t, "hello", *sm.Message)
	assert.Equal(t, "myapp", *sm.Appname)
	assert.Equal(t, "foo\nbar", *sm.ProcID)
}
