package output

import (
	"bytes"
	"encoding/json"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	"sync"
	"sync/atomic"

"github.com/antitbone/ja4/sentinel/api"
)

func TestStdoutWriter(t *testing.T) {
	w := NewStdoutWriter()
	if w == nil {
		t.Fatal("NewStdoutWriter() returned nil")
	}

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		DstIP:   "10.0.0.1",
		DstPort: 443,
		JA4:     "t13d1516h2_test",
	}

	// Write should not fail (but we can't easily test stdout output)
	err := w.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Close should be no-op
	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}
}

func TestFileWriter(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	w, err := NewFileWriter(testFile)
	if err != nil {
		t.Fatalf("NewFileWriter() error = %v", err)
	}
	defer w.Close()

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		DstIP:   "10.0.0.1",
		DstPort: 443,
		JA4:     "t13d1516h2_test",
	}

	err = w.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Close the writer to flush
	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	// Verify file was created and contains data
	data, err := os.ReadFile(testFile)
	if err != nil {
		t.Fatalf("Failed to read test file: %v", err)
	}

	if len(data) == 0 {
		t.Error("File is empty")
	}

	// Verify it's valid JSON
	var got api.LogRecord
	if err := json.Unmarshal(data, &got); err != nil {
		t.Errorf("Output is not valid JSON: %v", err)
	}

	if got.SrcIP != rec.SrcIP {
		t.Errorf("SrcIP = %v, want %v", got.SrcIP, rec.SrcIP)
	}
}

func TestFileWriter_CreatesDirectory(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "subdir", "nested", "test.log")

	w, err := NewFileWriter(testFile)
	if err != nil {
		t.Fatalf("NewFileWriter() error = %v", err)
	}
	defer w.Close()

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		DstIP:   "10.0.0.1",
		DstPort: 443,
		JA4:     "test",
	}

	err = w.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Verify file exists
	if _, err := os.Stat(testFile); os.IsNotExist(err) {
		t.Error("File was not created")
	}
}

func TestMultiWriter(t *testing.T) {
	mw := NewMultiWriter()
	if mw == nil {
		t.Fatal("NewMultiWriter() returned nil")
	}

	// Create a test writer that tracks writes
	var writeCount int
	testWriter := &testWriter{
		writeFunc: func(rec api.LogRecord) error {
			writeCount++
			return nil
		},
	}

	mw.Add(testWriter)
	mw.Add(NewStdoutWriter())

	rec := api.LogRecord{
		SrcIP: "192.168.1.1",
		JA4:   "test",
	}

	err := mw.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	if writeCount != 1 {
		t.Errorf("writeCount = %d, want 1", writeCount)
	}

	// CloseAll should not fail
	if err := mw.CloseAll(); err != nil {
		t.Errorf("CloseAll() error = %v", err)
	}
}

func TestMultiWriter_WriteError(t *testing.T) {
	mw := NewMultiWriter()

	// Create a writer that always fails
	failWriter := &testWriter{
		writeFunc: func(rec api.LogRecord) error {
			return os.ErrPermission
		},
	}

	mw.Add(failWriter)

	rec := api.LogRecord{SrcIP: "192.168.1.1"}
	err := mw.Write(rec)

	// Should return the last error
	if err != os.ErrPermission {
		t.Errorf("Write() error = %v, want %v", err, os.ErrPermission)
	}
}

func TestBuilder_NewFromConfig(t *testing.T) {
	builder := NewBuilder()

	tests := []struct {
		name    string
		config  api.AppConfig
		wantErr bool
	}{
		{
			name: "empty config defaults to stdout",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{},
			},
			wantErr: false,
		},
		{
			name: "stdout output",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{Type: "stdout", Enabled: true},
				},
			},
			wantErr: false,
		},
		{
			name: "disabled output ignored",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{Type: "stdout", Enabled: false},
				},
			},
			wantErr: false,
		},
		{
			name: "file output without path fails",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{Type: "file", Enabled: true, Params: map[string]string{}},
				},
			},
			wantErr: true,
		},
		{
			name: "unix socket without socket_path fails",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{Type: "unix_socket", Enabled: true, Params: map[string]string{}},
				},
			},
			wantErr: true,
		},
		{
			name: "unknown output type fails",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{Type: "unknown", Enabled: true},
				},
			},
			wantErr: true,
		},
		{
			name: "unix socket with custom AsyncBuffer",
			config: api.AppConfig{
				Core: api.Config{
					Interface:   "eth0",
					ListenPorts: []uint16{443},
				},
				Outputs: []api.OutputConfig{
					{
						Type:        "unix_socket",
						Enabled:     true,
						AsyncBuffer: 5000,
						Params:      map[string]string{"socket_path": "test.sock"},
					},
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			// Set up paths for tests that need them (only for valid configs)
			if !tt.wantErr {
				for i := range tt.config.Outputs {
					if tt.config.Outputs[i].Type == "file" {
						if tt.config.Outputs[i].Params == nil {
							tt.config.Outputs[i].Params = make(map[string]string)
						}
						tt.config.Outputs[i].Params["path"] = filepath.Join(tmpDir, "test.log")
					}
					if tt.config.Outputs[i].Type == "unix_socket" {
						if tt.config.Outputs[i].Params == nil {
							tt.config.Outputs[i].Params = make(map[string]string)
						}
						tt.config.Outputs[i].Params["socket_path"] = filepath.Join(tmpDir, "test.sock")
					}
				}
			}

			_, err := builder.NewFromConfig(tt.config)
			if (err != nil) != tt.wantErr {
				t.Errorf("NewFromConfig() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestUnixSocketWriter(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	// Create writer (socket doesn't need to exist yet)
	w, err := NewUnixSocketWriter(socketPath)
	if err != nil {
		t.Fatalf("NewUnixSocketWriter() error = %v", err)
	}
	defer w.Close()

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}

	// Write should queue the message (won't fail if socket doesn't exist)
	err = w.Write(rec)
	if err != nil {
		t.Logf("Write() error (expected if socket doesn't exist) = %v", err)
	}

	// Close should clean up properly
	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}
}

func TestUnixSocketWriterWithConfig(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	w, err := NewUnixSocketWriterWithConfig(socketPath, 1*time.Second, 1*time.Second, 100)
	if err != nil {
		t.Fatalf("NewUnixSocketWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	if w.dialTimeout != 1*time.Second {
		t.Errorf("dialTimeout = %v, want 1s", w.dialTimeout)
	}
	if w.writeTimeout != 1*time.Second {
		t.Errorf("writeTimeout = %v, want 1s", w.writeTimeout)
	}
}

func TestUnixSocketWriter_CloseTwice(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	w, err := NewUnixSocketWriter(socketPath)
	if err != nil {
		t.Fatalf("NewUnixSocketWriter() error = %v", err)
	}

	// First close
	if err := w.Close(); err != nil {
		t.Errorf("Close() first error = %v", err)
	}

	// Second close should be safe (no-op)
	if err := w.Close(); err != nil {
		t.Errorf("Close() second error = %v", err)
	}
}

func TestUnixSocketWriter_WriteAfterClose(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	w, err := NewUnixSocketWriter(socketPath)
	if err != nil {
		t.Fatalf("NewUnixSocketWriter() error = %v", err)
	}

	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	rec := api.LogRecord{SrcIP: "192.168.1.1"}
	err = w.Write(rec)
	if err == nil {
		t.Error("Write() after Close() should return error")
	}
}

// testWriter is a mock writer for testing
type testWriter struct {
	writeFunc func(api.LogRecord) error
	closeFunc func() error
}

func (w *testWriter) Write(rec api.LogRecord) error {
	if w.writeFunc != nil {
		return w.writeFunc(rec)
	}
	return nil
}

func (w *testWriter) Close() error {
	if w.closeFunc != nil {
		return w.closeFunc()
	}
	return nil
}

// Test to verify LogRecord JSON serialization
func TestLogRecordJSONSerialization(t *testing.T) {
	rec := api.LogRecord{
		SrcIP:      "192.168.1.100",
		SrcPort:    54321,
		DstIP:      "10.0.0.1",
		DstPort:    443,
		IPTTL:      64,
		IPTotalLen: 512,
		IPID:       12345,
		IPDF:       true,
		TCPWindow:  65535,
		TCPOptions: "MSS,WS,SACK,TS",
		// New fields per architecture.yml
		ConnID:     "flow-abc123",
		SensorID:   "sensor-01",
		TLSVersion: "1.3",
		SNI:        "example.com",
		ALPN:       "h2",
		// Fingerprints - note: JA4Hash is NOT in LogRecord per architecture
		JA4:       "t13d1516h2_8daaf6152771_02cb136f2775",
		JA3:       "771,4865-4866-4867,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
		JA3Hash:   "a0e6f06c7a6d15e5e3f0f0e6f06c7a6d",
		Timestamp: time.Now().UnixNano(),
	}

	data, err := json.Marshal(rec)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	// Verify it can be unmarshaled
	var got api.LogRecord
	if err := json.Unmarshal(data, &got); err != nil {
		t.Errorf("json.Unmarshal() error = %v", err)
	}

	// Verify key fields
	if got.SrcIP != rec.SrcIP {
		t.Errorf("SrcIP = %v, want %v", got.SrcIP, rec.SrcIP)
	}
	if got.JA4 != rec.JA4 {
		t.Errorf("JA4 = %v, want %v", got.JA4, rec.JA4)
	}
	// Verify JA4Hash is NOT present (architecture decision)
	// JA4Hash field doesn't exist in LogRecord anymore
	// Verify new fields
	if got.ConnID != rec.ConnID {
		t.Errorf("ConnID = %v, want %v", got.ConnID, rec.ConnID)
	}
	if got.SNI != rec.SNI {
		t.Errorf("SNI = %v, want %v", got.SNI, rec.SNI)
	}
}

// Test to verify optional fields are omitted when empty
func TestLogRecordOptionalFieldsOmitted(t *testing.T) {
	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		DstIP:   "10.0.0.1",
		DstPort: 443,
		// Optional fields not set
		TCPMSS:    nil,
		TCPWScale: nil,
		JA3:       "",
		JA3Hash:   "",
	}

	data, err := json.Marshal(rec)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	// Check that optional fields are not present in JSON
	jsonStr := string(data)
	if contains(jsonStr, `"tcp_meta_mss"`) {
		t.Error("tcp_meta_mss should be omitted when nil")
	}
	if contains(jsonStr, `"tcp_meta_window_scale"`) {
		t.Error("tcp_meta_window_scale should be omitted when nil")
	}
}

func contains(s, substr string) bool {
	return bytes.Contains([]byte(s), []byte(substr))
}

// TestUnixSocketWriter_ErrorCallback tests that errors are reported via callback
func TestUnixSocketWriter_ErrorCallback(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "nonexistent.sock")

	// Track callback invocations — protected by mu (callback runs in processQueue goroutine)
	var mu sync.Mutex
	var errorCalls []struct {
		path    string
		err     error
		attempt int
	}

	callback := func(path string, err error, attempt int) {
		mu.Lock()
		errorCalls = append(errorCalls, struct {
			path    string
			err     error
			attempt int
		}{path, err, attempt})
		mu.Unlock()
	}

	w, err := NewUnixSocketWriterWithConfig(
		socketPath,
		100*time.Millisecond,
		100*time.Millisecond,
		10,
		WithErrorCallback(callback),
	)
	if err != nil {
		t.Fatalf("NewUnixSocketWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}

	// Write should queue the message
	err = w.Write(rec)
	if err != nil {
		t.Errorf("Write() unexpected error = %v", err)
	}

	// Wait for queue processor to attempt write and trigger callback
	time.Sleep(500 * time.Millisecond)

	// Callback should have been invoked at least once
	mu.Lock()
	calls := make([]struct {
		path    string
		err     error
		attempt int
	}, len(errorCalls))
	copy(calls, errorCalls)
	mu.Unlock()

	if len(calls) == 0 {
		t.Error("ErrorCallback was not invoked")
	} else {
		lastCall := calls[len(calls)-1]
		if lastCall.path != socketPath {
			t.Errorf("Callback path = %v, want %v", lastCall.path, socketPath)
		}
		if lastCall.err == nil {
			t.Error("Callback err should not be nil")
		}
		if lastCall.attempt < 1 {
			t.Errorf("Callback attempt = %d, want >= 1", lastCall.attempt)
		}
	}
}

// TestBuilder_WithErrorCallback tests that the builder propagates error callbacks
func TestBuilder_WithErrorCallback(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	callback := func(path string, err error, attempt int) {
		// Callback tracked for verification
	}

	builder := NewBuilder().WithErrorCallback(callback)

	config := api.AppConfig{
		Core: api.Config{
			Interface:   "eth0",
			ListenPorts: []uint16{443},
		},
		Outputs: []api.OutputConfig{
			{
				Type:        "unix_socket",
				Enabled:     true,
				AsyncBuffer: 100,
				Params:      map[string]string{"socket_path": socketPath},
			},
		},
	}

	writer, err := builder.NewFromConfig(config)
	if err != nil {
		t.Fatalf("NewFromConfig() error = %v", err)
	}

	// Verify writer is a MultiWriter
	mw, ok := writer.(*MultiWriter)
	if !ok {
		t.Fatal("Writer is not a MultiWriter")
	}

	// Verify the UnixSocketWriter has the callback set
	if len(mw.writers) != 1 {
		t.Fatalf("Expected 1 writer, got %d", len(mw.writers))
	}

	unixWriter, ok := mw.writers[0].(*UnixSocketWriter)
	if !ok {
		t.Fatal("Writer is not a UnixSocketWriter")
	}

	if unixWriter.errorCallback == nil {
		t.Error("UnixSocketWriter.errorCallback is nil")
	}

	_ = writer
}

// TestUnixSocketWriter_NoCallback tests that writer works without callback
func TestUnixSocketWriter_NoCallback(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "nonexistent.sock")

	// Create writer without callback
	w, err := NewUnixSocketWriter(socketPath)
	if err != nil {
		t.Fatalf("NewUnixSocketWriter() error = %v", err)
	}
	defer w.Close()

	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}

	// Write should not panic even without callback
	err = w.Write(rec)
	if err != nil {
		t.Logf("Write() error (expected) = %v", err)
	}

	// Give queue processor time to run
	time.Sleep(100 * time.Millisecond)

	// Should not panic
}

// TestUnixSocketWriter_CallbackResetOnSuccess tests that failure counter resets on success
func TestUnixSocketWriter_CallbackResetOnSuccess(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	// Create a real DGRAM (unixgram) socket
	addr, err := net.ResolveUnixAddr("unixgram", socketPath)
	if err != nil {
		t.Fatalf("Failed to resolve unixgram address: %v", err)
	}
	listener, err := net.ListenUnixgram("unixgram", addr)
	if err != nil {
		t.Fatalf("Failed to create unixgram socket: %v", err)
	}
	defer listener.Close()

	// Start a goroutine to read datagrams
	done := make(chan struct{})
	go func() {
		buf := make([]byte, 65536)
		for {
			select {
			case <-done:
				return
			default:
			}
			listener.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
			_, _, _ = listener.ReadFrom(buf)
		}
	}()
	defer close(done)

	var errorCalls int
	callback := func(path string, err error, attempt int) {
		errorCalls++
	}

	w, err := NewUnixSocketWriterWithConfig(
		socketPath,
		100*time.Millisecond,
		100*time.Millisecond,
		10,
		WithErrorCallback(callback),
	)
	if err != nil {
		t.Fatalf("NewUnixSocketWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	// Write successfully
	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}

	err = w.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Wait for write to complete
	time.Sleep(200 * time.Millisecond)

	// Callback should not have been called since connection succeeded
	if errorCalls > 0 {
		t.Errorf("ErrorCallback called %d times, want 0 for successful connection", errorCalls)
	}
}

// TestFileWriter_Reopen tests the Reopen method for logrotate support
func TestFileWriter_Reopen(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	w, err := NewFileWriter(testFile)
	if err != nil {
		t.Fatalf("NewFileWriter() error = %v", err)
	}

	// Write initial data
	rec1 := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test1",
	}

	err = w.Write(rec1)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Reopen the file (for logrotate - file is typically moved externally)
	err = w.Reopen()
	if err != nil {
		t.Errorf("Reopen() error = %v", err)
	}

	// Write more data after reopen
	rec2 := api.LogRecord{
		SrcIP:   "192.168.1.2",
		SrcPort: 54321,
		JA4:     "test2",
	}

	err = w.Write(rec2)
	if err != nil {
		t.Errorf("Write() after reopen error = %v", err)
	}

	// Close and verify
	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	// Read the file - should contain both records (Reopen uses O_APPEND)
	data, err := os.ReadFile(testFile)
	if err != nil {
		t.Fatalf("Failed to read file: %v", err)
	}

	// Parse JSON lines
	lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))
	if len(lines) != 2 {
		t.Fatalf("Expected 2 lines, got %d", len(lines))
	}

	// Verify second record
	var got api.LogRecord
	if err := json.Unmarshal(lines[1], &got); err != nil {
		t.Errorf("Invalid JSON on line 2: %v", err)
	}

	if got.SrcIP != rec2.SrcIP {
		t.Errorf("SrcIP = %v, want %v", got.SrcIP, rec2.SrcIP)
	}
}

// TestFileWriter_Rotate tests the log rotation functionality
func TestFileWriter_Rotate(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	// Create writer with very small max size to trigger rotation
	// Minimum useful size is ~100 bytes for a log record
	w, err := NewFileWriterWithConfig(testFile, 200, 3)
	if err != nil {
		t.Fatalf("NewFileWriterWithConfig() error = %v", err)
	}

	// Write multiple records to trigger rotation
	records := []api.LogRecord{
		{SrcIP: "192.168.1.1", SrcPort: 1111, JA4: "record1"},
		{SrcIP: "192.168.1.2", SrcPort: 2222, JA4: "record2"},
		{SrcIP: "192.168.1.3", SrcPort: 3333, JA4: "record3"},
		{SrcIP: "192.168.1.4", SrcPort: 4444, JA4: "record4"},
	}

	for i, rec := range records {
		err = w.Write(rec)
		if err != nil {
			t.Errorf("Write() record %d error = %v", i, err)
		}
	}

	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	// Check that rotation occurred (backup file should exist)
	backupFile := testFile + ".1"
	if _, err := os.Stat(backupFile); os.IsNotExist(err) {
		t.Log("Note: Rotation may not have occurred if total data < maxSize")
	}

	// Verify main file exists and has content
	if _, err := os.Stat(testFile); os.IsNotExist(err) {
		t.Errorf("Main file %s does not exist", testFile)
	}
}

// TestFileWriter_Rotate_MaxBackups tests that old backups are cleaned up
func TestFileWriter_Rotate_MaxBackups(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	// Create writer with small max size and only 2 backups
	w, err := NewFileWriterWithConfig(testFile, 150, 2)
	if err != nil {
		t.Fatalf("NewFileWriterWithConfig() error = %v", err)
	}

	// Write enough records to trigger multiple rotations
	for i := 0; i < 10; i++ {
		rec := api.LogRecord{
			SrcIP:   "192.168.1.1",
			SrcPort: uint16(1000 + i),
			JA4:     "test",
		}
		err = w.Write(rec)
		if err != nil {
			t.Errorf("Write() error = %v", err)
		}
	}

	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	// Count backup files
	backupCount := 0
	for i := 1; i <= 5; i++ {
		backupPath := testFile + "." + string(rune('0'+i))
		if _, err := os.Stat(backupPath); err == nil {
			backupCount++
		}
	}

	// Should have at most 2 backups
	if backupCount > 2 {
		t.Errorf("Too many backup files: %d, want <= 2", backupCount)
	}
}

// TestFileWriter_Reopen_Error tests Reopen after external file removal
func TestFileWriter_Reopen_Error(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	w, err := NewFileWriter(testFile)
	if err != nil {
		t.Fatalf("NewFileWriter() error = %v", err)
	}

	// Write initial data
	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}
	err = w.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Remove the file externally (simulating logrotate move)
	os.Remove(testFile)

	// Reopen should succeed - it will create a new file
	err = w.Reopen()
	if err != nil {
		t.Errorf("Reopen() should succeed after file removal, error = %v", err)
	}

	if err := w.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}
}

// TestFileWriter_NewFileWriterWithConfig tests custom configuration
func TestFileWriter_NewFileWriterWithConfig(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	// Test with custom max size and backups
	w, err := NewFileWriterWithConfig(testFile, 50*1024*1024, 5)
	if err != nil {
		t.Fatalf("NewFileWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	if w.maxSize != 50*1024*1024 {
		t.Errorf("maxSize = %d, want %d", w.maxSize, 50*1024*1024)
	}
	if w.maxBackups != 5 {
		t.Errorf("maxBackups = %d, want 5", w.maxBackups)
	}
}

// TestFileWriter_NewFileWriterWithConfig_InvalidPath tests error handling
func TestFileWriter_NewFileWriterWithConfig_InvalidPath(t *testing.T) {
	// Try to create file in a path that should fail (e.g., /proc which is read-only)
	_, err := NewFileWriterWithConfig("/proc/test/test.log", 1024, 3)
	if err == nil {
		t.Error("NewFileWriterWithConfig() with invalid path should return error")
	}
}

// TestMultiWriter_Reopen tests Reopen on MultiWriter
func TestMultiWriter_Reopen(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.log")

	mw := NewMultiWriter()

	// Add a FileWriter (which is Reopenable)
	fw, err := NewFileWriter(testFile)
	if err != nil {
		t.Fatalf("NewFileWriter() error = %v", err)
	}
	mw.Add(fw)

	// Add a StdoutWriter (which is NOT Reopenable)
	mw.Add(NewStdoutWriter())

	// Write initial data
	rec := api.LogRecord{
		SrcIP:   "192.168.1.1",
		SrcPort: 12345,
		JA4:     "test",
	}

	err = mw.Write(rec)
	if err != nil {
		t.Errorf("Write() error = %v", err)
	}

	// Reopen should work (FileWriter is reopenable, StdoutWriter is skipped)
	err = mw.Reopen()
	if err != nil {
		t.Errorf("Reopen() error = %v", err)
	}

	// Write after reopen
	rec2 := api.LogRecord{
		SrcIP:   "192.168.1.2",
		SrcPort: 54321,
		JA4:     "test2",
	}

	err = mw.Write(rec2)
	if err != nil {
		t.Errorf("Write() after reopen error = %v", err)
	}

	if err := mw.CloseAll(); err != nil {
		t.Errorf("CloseAll() error = %v", err)
	}
}

// TestUnixSocketWriter_QueueFull tests behavior when queue is full
func TestUnixSocketWriter_QueueFull(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "test.sock")

	// Create writer with very small queue
	w, err := NewUnixSocketWriterWithConfig(socketPath, 10*time.Millisecond, 10*time.Millisecond, 2)
	if err != nil {
		t.Fatalf("NewUnixSocketWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	// Fill the queue with records
	for i := 0; i < 10; i++ {
		rec := api.LogRecord{
			SrcIP:   "192.168.1.1",
			SrcPort: uint16(1000 + i),
			JA4:     "test",
		}
		_ = w.Write(rec) // May succeed or fail depending on queue state
	}

	// Should not panic - queue full messages are dropped
}

// TestUnixSocketWriter_ReconnectBackoff tests exponential backoff behavior
func TestUnixSocketWriter_ReconnectBackoff(t *testing.T) {
	tmpDir := t.TempDir()
	socketPath := filepath.Join(tmpDir, "nonexistent.sock")

	var errorCount atomic.Int32
	callback := func(path string, err error, attempt int) {
		errorCount.Add(1)
	}

	w, err := NewUnixSocketWriterWithConfig(
		socketPath,
		10*time.Millisecond,
		10*time.Millisecond,
		5,
		WithErrorCallback(callback),
	)
	if err != nil {
		t.Fatalf("NewUnixSocketWriterWithConfig() error = %v", err)
	}
	defer w.Close()

	// Write multiple records to trigger reconnection attempts
	for i := 0; i < 3; i++ {
		rec := api.LogRecord{
			SrcIP:   "192.168.1.1",
			SrcPort: uint16(1000 + i),
			JA4:     "test",
		}
		_ = w.Write(rec)
	}

	// Wait for queue processor to attempt writes
	time.Sleep(500 * time.Millisecond)

	// Should have attempted reconnection
	if errorCount.Load() == 0 {
		t.Error("Expected at least one error callback for nonexistent socket")
	}
}

// TestUnixSocketWriter_WriteConcurrentClose_NoPanic verifies the Bug 12 fix:
// concurrent Write() and Close() must not panic with "send on closed channel".
// The test spins many goroutines calling Write() while a Close() races against them.
func TestUnixSocketWriter_WriteConcurrentClose_NoPanic(t *testing.T) {
const workers = 20
const iterations = 100

for trial := 0; trial < 5; trial++ {
w, err := NewUnixSocketWriterWithConfig(
"/tmp/bug12_test.sock",
time.Second, time.Second, 128,
)
if err != nil {
t.Fatalf("NewUnixSocketWriterWithConfig: %v", err)
}

rec := api.LogRecord{SrcIP: "1.2.3.4", JA4: "t13d_test"}
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
_ = w.Write(rec) // may return error but must not panic
}
}()
}

// Close races with the writes.
time.Sleep(time.Millisecond)
if err := w.Close(); err != nil {
t.Errorf("Close() error: %v", err)
}
wg.Wait()
}
}
