package file

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"github.com/antitbone/ja4/correlator/internal/domain"
)

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	log := domain.CorrelatedLog{
		SrcIP:      "192.168.1.1",
		SrcPort:    8080,
		Correlated: 1,
	}

	if err := sink.Write(context.Background(), log); err != nil {
		t.Fatalf("failed to write: %v", err)
	}

	if err := sink.Flush(context.Background()); err != nil {
		t.Fatalf("failed to flush: %v", err)
	}

	// Verify file exists and contains data
	data, err := os.ReadFile(testPath)
	if err != nil {
		t.Fatalf("failed to read file: %v", err)
	}

	if len(data) == 0 {
		t.Error("expected non-empty file")
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	log := domain.CorrelatedLog{
		SrcIP:      "192.168.1.1",
		SrcPort:    8080,
		Correlated: 1,
	}

	if err := sink.Write(context.Background(), log); err != nil {
		t.Fatalf("failed to write: %v", err)
	}

	// Must be visible immediately without Flush()
	data, err := os.ReadFile(testPath)
	if err != nil {
		t.Fatalf("failed to read file: %v", err)
	}
	if len(data) == 0 {
		t.Error("expected data to be present immediately after Write without Flush")
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	for i := 0; i < 5; i++ {
		log := domain.CorrelatedLog{
			SrcIP:   "192.168.1.1",
			SrcPort: 8080 + i,
		}
		if err := sink.Write(context.Background(), log); err != nil {
			t.Fatalf("failed to write: %v", err)
		}
	}

	sink.Close()

	// Verify file has 5 lines
	data, err := os.ReadFile(testPath)
	if err != nil {
		t.Fatalf("failed to read file: %v", err)
	}

	lines := 0
	for _, b := range data {
		if b == '\n' {
			lines++
		}
	}

	if lines != 5 {
		t.Errorf("expected 5 lines, got %d", lines)
	}
}

func TestFileSink_Name(t *testing.T) {
	sink, err := NewFileSink(Config{Path: "/tmp/test.log"})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}

	if sink.Name() != "file" {
		t.Errorf("expected name 'file', got %s", sink.Name())
	}
}

func TestFileSink_ValidateFilePath(t *testing.T) {
	tests := []struct {
		name    string
		path    string
		wantErr bool
	}{
		{"empty path", "", true},
		{"valid /var/log/logcorrelator", "/var/log/logcorrelator/test.log", false},
		{"valid /var/log", "/var/log/test.log", false},
		{"valid /tmp", "/tmp/test.log", false},
		{"reject lookalike /var/logevil", "/var/logevil/test.log", true},
		{"invalid directory", "/etc/logcorrelator/test.log", true},
		{"relative path", "test.log", false}, // Allowed for testing
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateFilePath(tt.path)
			if (err != nil) != tt.wantErr {
				t.Errorf("validateFilePath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr)
			}
		})
	}
}

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

	sink := &FileSink{
		config: Config{Path: testPath},
	}

	err := sink.openFile()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	defer sink.Close()

	if sink.file == nil {
		t.Error("expected file to be opened")
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	// Write should open file automatically
	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	err = sink.Write(context.Background(), log)
	if err != nil {
		t.Fatalf("failed to write: %v", err)
	}

	// Verify file was created
	if _, err := os.Stat(testPath); os.IsNotExist(err) {
		t.Error("expected file to be created")
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	// Flush before any write should not error
	err = sink.Flush(context.Background())
	if err != nil {
		t.Errorf("expected no error on flush before open, got %v", err)
	}
}

func TestFileSink_InvalidPath(t *testing.T) {
	// Test with invalid path (outside allowed directories)
	_, err := NewFileSink(Config{Path: "/etc/../passwd"})
	if err == nil {
		t.Error("expected error for invalid path")
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}

	// Write initial data
	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	if err := sink.Write(context.Background(), log); err != nil {
		t.Fatalf("failed to write: %v", err)
	}

	// Reopen should close and reopen the file
	err = sink.Reopen()
	if err != nil {
		t.Errorf("expected no error on Reopen, got %v", err)
	}

	// Write after reopen
	log2 := domain.CorrelatedLog{SrcIP: "192.168.1.2", SrcPort: 8081}
	if err := sink.Write(context.Background(), log2); err != nil {
		t.Fatalf("failed to write after reopen: %v", err)
	}

	sink.Close()

	// Verify both writes are present
	data, err := os.ReadFile(testPath)
	if err != nil {
		t.Fatalf("failed to read file: %v", err)
	}

	lines := 0
	for _, b := range data {
		if b == '\n' {
			lines++
		}
	}

	if lines != 2 {
		t.Errorf("expected 2 lines after reopen, got %d", lines)
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}

	// Close should succeed
	err = sink.Close()
	if err != nil {
		t.Errorf("expected no error on Close, got %v", err)
	}

	// Write after close should fail or reopen
	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	err = sink.Write(context.Background(), log)
	if err != nil {
		// Expected - file was closed
		t.Logf("write after close returned error (expected): %v", err)
	}
}

func TestFileSink_EmptyPath(t *testing.T) {
	_, err := NewFileSink(Config{Path: ""})
	if err == nil {
		t.Error("expected error for empty path")
	}
}

func TestFileSink_WhitespacePath(t *testing.T) {
	_, err := NewFileSink(Config{Path: "   "})
	if err == nil {
		t.Error("expected error for whitespace-only path")
	}
}

func TestFileSink_ValidateFilePath_AllowedRoots(t *testing.T) {
	// Test paths under allowed roots
	allowedPaths := []string{
		"/var/log/logcorrelator/correlated.log",
		"/var/log/test.log",
		"/tmp/test.log",
		"/tmp/subdir/test.log",
		"relative/path/test.log",
		"./test.log",
	}

	for _, path := range allowedPaths {
		err := validateFilePath(path)
		if err != nil {
			t.Errorf("validateFilePath(%q) unexpected error: %v", path, err)
		}
	}
}

func TestFileSink_ValidateFilePath_RejectedPaths(t *testing.T) {
	// Test paths that should be rejected
	rejectedPaths := []string{
		"",
		"   ",
		"/etc/passwd",
		"/etc/logcorrelator/test.log",
		"/root/test.log",
		"/home/user/test.log",
		"/var/logevil/test.log",
	}

	for _, path := range rejectedPaths {
		err := validateFilePath(path)
		if err == nil {
			t.Errorf("validateFilePath(%q) should have been rejected", path)
		}
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	done := make(chan bool)
	for i := 0; i < 10; i++ {
		go func(n int) {
			log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080 + n}
			sink.Write(context.Background(), log)
			done <- true
		}(i)
	}

	for i := 0; i < 10; i++ {
		<-done
	}

	// Verify all writes completed
	data, err := os.ReadFile(testPath)
	if err != nil {
		t.Fatalf("failed to read file: %v", err)
	}

	lines := 0
	for _, b := range data {
		if b == '\n' {
			lines++
		}
	}

	if lines != 10 {
		t.Errorf("expected 10 lines from concurrent writes, got %d", lines)
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
	if err := sink.Write(context.Background(), log); err != nil {
		t.Fatalf("failed to write: %v", err)
	}

	// Flush should succeed
	err = sink.Flush(context.Background())
	if err != nil {
		t.Errorf("expected no error on Flush, got %v", err)
	}
}

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

	sink, err := NewFileSink(Config{Path: testPath})
	if err != nil {
		t.Fatalf("failed to create sink: %v", err)
	}
	defer sink.Close()

	// Create a log with unmarshalable data (channel)
	log := domain.CorrelatedLog{
		SrcIP:   "192.168.1.1",
		SrcPort: 8080,
		Fields:  map[string]any{"chan": make(chan int)},
	}

	err = sink.Write(context.Background(), log)
	if err == nil {
		t.Error("expected error when marshaling unmarshalable data")
	}
}

// TestFileSink_CloseTwice tests that closing an already-closed sink does not error.
func TestFileSink_CloseTwice(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "test.log")

sink, err := NewFileSink(Config{Path: testPath})
if err != nil {
t.Fatalf("failed to create sink: %v", err)
}

if err := sink.Close(); err != nil {
t.Errorf("first Close() should not error, got: %v", err)
}

// After close, file is nil, so second close should return nil
if err := sink.Close(); err != nil {
t.Errorf("second Close() on already-closed sink should not error, got: %v", err)
}
}

// TestFileSink_WriteAfterClose tests that Write after Close re-opens the file.
func TestFileSink_WriteAfterCloseReopens(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "test.log")

sink, err := NewFileSink(Config{Path: testPath})
if err != nil {
t.Fatalf("failed to create sink: %v", err)
}

if err := sink.Close(); err != nil {
t.Fatalf("Close() failed: %v", err)
}

// Write after close: FileSink.Write reopens the file when file == nil
log := domain.CorrelatedLog{SrcIP: "1.2.3.4", SrcPort: 80}
if err := sink.Write(context.Background(), log); err != nil {
t.Errorf("Write after close should succeed (auto-reopen), got: %v", err)
}

// Verify data was written
data, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
if len(data) == 0 {
t.Error("expected data to be present after write on re-opened file")
}
}

// TestFileSink_ReopenAfterWrite tests Reopen then write produces correct output.
func TestFileSink_ReopenThenWrite(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "test.log")

sink, err := NewFileSink(Config{Path: testPath})
if err != nil {
t.Fatalf("failed to create sink: %v", err)
}
defer sink.Close()

// Write before reopen
log1 := domain.CorrelatedLog{SrcIP: "1.1.1.1", SrcPort: 80}
if err := sink.Write(context.Background(), log1); err != nil {
t.Fatalf("first Write failed: %v", err)
}

// Simulate log rotation
if err := sink.Reopen(); err != nil {
t.Fatalf("Reopen failed: %v", err)
}

// Write after reopen
log2 := domain.CorrelatedLog{SrcIP: "2.2.2.2", SrcPort: 443}
if err := sink.Write(context.Background(), log2); err != nil {
t.Fatalf("second Write failed: %v", err)
}

sink.Close()

data, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}

lines := 0
for _, b := range data {
if b == '\n' {
lines++
}
}
if lines != 2 {
t.Errorf("expected 2 lines after reopen+write, got %d", lines)
}
}
