package capture

import (
	"net"
	"strings"
	"testing"
	"time"

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

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	"github.com/google/gopacket/pcap"
)

func TestCaptureImpl_Run_EmptyInterface(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	cfg := api.Config{
		Interface:   "",
		ListenPorts: []uint16{443},
	}

	out := make(chan api.RawPacket, 10)
	err := c.Run(cfg, out)

	if err == nil {
		t.Error("Run() with empty interface should return error")
	}
	if err.Error() != "interface cannot be empty" {
		t.Errorf("Run() error = %v, want 'interface cannot be empty'", err)
	}
}

func TestCaptureImpl_Run_NonExistentInterface(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	cfg := api.Config{
		Interface:   "nonexistent_interface_xyz123",
		ListenPorts: []uint16{443},
	}

	out := make(chan api.RawPacket, 10)
	err := c.Run(cfg, out)

	if err == nil {
		t.Error("Run() with non-existent interface should return error")
	}
}

func TestCaptureImpl_Run_InvalidBPFFilter(t *testing.T) {
	// Get a real interface name
	ifaces, err := pcap.FindAllDevs()
	if err != nil || len(ifaces) == 0 {
		t.Skip("No network interfaces available for testing")
	}

	c := New()
	cfg := api.Config{
		Interface:   ifaces[0].Name,
		ListenPorts: []uint16{443},
		BPFFilter:   "invalid; rm -rf /", // Invalid characters
	}

	out := make(chan api.RawPacket, 10)
	err = c.Run(cfg, out)

	if err == nil {
		t.Error("Run() with invalid BPF filter should return error")
	}
}

func TestCaptureImpl_Run_ChannelFull_DropsPackets(t *testing.T) {
	// This test verifies that when the output channel is full,
	// packets are dropped gracefully (non-blocking write)

	// We can't easily test the full Run() loop without real interfaces,
	// but we can verify the channel behavior with a small buffer
	out := make(chan api.RawPacket, 1)

	// Fill the channel
	out <- api.RawPacket{Data: []byte{1, 2, 3}, Timestamp: time.Now().UnixNano()}

	// Channel should be full now, select default should trigger
	done := make(chan bool)
	go func() {
		select {
		case out <- api.RawPacket{Data: []byte{4, 5, 6}, Timestamp: time.Now().UnixNano()}:
			done <- false // Would block
		default:
			done <- true // Dropped as expected
		}
	}()

	dropped := <-done
	if !dropped {
		t.Error("Expected packet to be dropped when channel is full")
	}
}

func TestPacketToRawPacket(t *testing.T) {
	t.Run("valid_packet", func(t *testing.T) {
		// Create a simple TCP packet
		eth := layers.Ethernet{
			SrcMAC:       []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55},
			DstMAC:       []byte{0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB},
			EthernetType: layers.EthernetTypeIPv4,
		}
		ip := layers.IPv4{
			Version:  4,
			TTL:      64,
			Protocol: layers.IPProtocolTCP,
			SrcIP:    []byte{192, 168, 1, 1},
			DstIP:    []byte{10, 0, 0, 1},
		}
		tcp := layers.TCP{
			SrcPort: 12345,
			DstPort: 443,
		}
		tcp.SetNetworkLayerForChecksum(&ip)

		buf := gopacket.NewSerializeBuffer()
		opts := gopacket.SerializeOptions{}
		gopacket.SerializeLayers(buf, opts, &eth, &ip, &tcp)

		packet := gopacket.NewPacket(buf.Bytes(), layers.LinkTypeEthernet, gopacket.Default)
		
		// Create capture instance for method call
		c := New()
		rawPkt := c.packetToRawPacket(packet)

		if rawPkt == nil {
			t.Fatal("packetToRawPacket() returned nil for valid packet")
		}
		if len(rawPkt.Data) == 0 {
			t.Error("packetToRawPacket() returned empty data")
		}
		if rawPkt.Timestamp == 0 {
			t.Error("packetToRawPacket() returned zero timestamp")
		}
	})

	t.Run("empty_packet", func(t *testing.T) {
		// Create packet with no data
		packet := gopacket.NewPacket([]byte{}, layers.LinkTypeEthernet, gopacket.Default)
		
		c := New()
		rawPkt := c.packetToRawPacket(packet)

		if rawPkt != nil {
			t.Error("packetToRawPacket() should return nil for empty packet")
		}
	})

	t.Run("nil_packet", func(t *testing.T) {
		// packetToRawPacket will panic with nil packet due to Metadata() call
		// This is expected behavior - the function is not designed to handle nil
		defer func() {
			if r := recover(); r == nil {
				t.Error("packetToRawPacket() with nil packet should panic")
			}
		}()
		c := New()
		var packet gopacket.Packet
		_ = c.packetToRawPacket(packet)
	})
}

func TestGetInterfaceNames(t *testing.T) {
	t.Run("empty_list", func(t *testing.T) {
		names := getInterfaceNames([]pcap.Interface{})
		if len(names) != 0 {
			t.Errorf("getInterfaceNames() with empty list = %v, want []", names)
		}
	})

	t.Run("single_interface", func(t *testing.T) {
		ifaces := []pcap.Interface{
			{Name: "eth0"},
		}
		names := getInterfaceNames(ifaces)
		if len(names) != 1 || names[0] != "eth0" {
			t.Errorf("getInterfaceNames() = %v, want [eth0]", names)
		}
	})

	t.Run("multiple_interfaces", func(t *testing.T) {
		ifaces := []pcap.Interface{
			{Name: "eth0"},
			{Name: "lo"},
			{Name: "docker0"},
		}
		names := getInterfaceNames(ifaces)
		if len(names) != 3 {
			t.Errorf("getInterfaceNames() returned %d names, want 3", len(names))
		}
		expected := []string{"eth0", "lo", "docker0"}
		for i, name := range names {
			if name != expected[i] {
				t.Errorf("getInterfaceNames()[%d] = %s, want %s", i, name, expected[i])
			}
		}
	})
}

func TestValidateBPFFilter(t *testing.T) {
	tests := []struct {
		name    string
		filter  string
		wantErr bool
	}{
		{
			name:    "empty filter",
			filter:  "",
			wantErr: false,
		},
		{
			name:    "valid simple filter",
			filter:  "tcp port 443",
			wantErr: false,
		},
		{
			name:    "valid complex filter",
			filter:  "(tcp port 443) or (tcp port 8443)",
			wantErr: false,
		},
		{
			name:    "filter with special chars",
			filter:  "tcp port 443 and host 192.168.1.1",
			wantErr: false,
		},
		{
			name:    "too long filter",
			filter:  string(make([]byte, MaxBPFFilterLength+1)),
			wantErr: true,
		},
		{
			name:    "unbalanced parentheses - extra open",
			filter:  "(tcp port 443",
			wantErr: true,
		},
		{
			name:    "unbalanced parentheses - extra close",
			filter:  "tcp port 443)",
			wantErr: true,
		},
		{
			name:    "invalid characters - semicolon",
			filter:  "tcp port 443; rm -rf /",
			wantErr: true,
		},
		{
			name:    "invalid characters - backtick",
			filter:  "tcp port `whoami`",
			wantErr: true,
		},
		{
			name:    "invalid characters - dollar",
			filter:  "tcp port $HOME",
			wantErr: true,
		},
	}

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

func TestJoinString(t *testing.T) {
	tests := []struct {
		name  string
		parts []string
		sep   string
		want  string
	}{
		{
			name:  "empty slice",
			parts: []string{},
			sep:   ") or (",
			want:  "",
		},
		{
			name:  "single element",
			parts: []string{"tcp port 443"},
			sep:   ") or (",
			want:  "tcp port 443",
		},
		{
			name:  "multiple elements",
			parts: []string{"tcp port 443", "tcp port 8443"},
			sep:   ") or (",
			want:  "tcp port 443) or (tcp port 8443",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := joinString(tt.parts, tt.sep)
			if got != tt.want {
				t.Errorf("joinString() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestNewCapture(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}
	if c.snapLen != DefaultSnapLen {
		t.Errorf("snapLen = %d, want %d", c.snapLen, DefaultSnapLen)
	}
	if c.promisc != DefaultPromiscuous {
		t.Errorf("promisc = %v, want %v", c.promisc, DefaultPromiscuous)
	}
}

func TestNewWithSnapLen(t *testing.T) {
	tests := []struct {
		name        string
		snapLen     int
		wantSnapLen int
	}{
		{
			name:        "valid snapLen",
			snapLen:     2048,
			wantSnapLen: 2048,
		},
		{
			name:        "zero snapLen uses default",
			snapLen:     0,
			wantSnapLen: DefaultSnapLen,
		},
		{
			name:        "negative snapLen uses default",
			snapLen:     -100,
			wantSnapLen: DefaultSnapLen,
		},
		{
			name:        "too large snapLen uses default",
			snapLen:     100000,
			wantSnapLen: DefaultSnapLen,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := NewWithSnapLen(tt.snapLen)
			if c == nil {
				t.Fatal("NewWithSnapLen() returned nil")
			}
			if c.snapLen != tt.wantSnapLen {
				t.Errorf("snapLen = %d, want %d", c.snapLen, tt.wantSnapLen)
			}
		})
	}
}

func TestCaptureImpl_Close(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	// Close should not panic on fresh instance
	if err := c.Close(); err != nil {
		t.Errorf("Close() error = %v", err)
	}

	// Multiple closes should be safe
	if err := c.Close(); err != nil {
		t.Errorf("Close() second call error = %v", err)
	}
}

func TestValidateBPFFilter_BalancedParentheses(t *testing.T) {
	// Test various balanced parentheses scenarios
	validFilters := []string{
		"(tcp port 443)",
		"((tcp port 443))",
		"(tcp port 443) or (tcp port 8443)",
		"((tcp port 443) or (tcp port 8443))",
		"(tcp port 443 and host 1.2.3.4) or (tcp port 8443)",
	}

	for _, filter := range validFilters {
		t.Run(filter, func(t *testing.T) {
			if err := validateBPFFilter(filter); err != nil {
				t.Errorf("validateBPFFilter(%q) unexpected error = %v", filter, err)
			}
		})
	}
}

func TestCaptureImpl_detectLocalIPs(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	t.Run("any_interface", func(t *testing.T) {
		ips, err := c.detectLocalIPs("any")
		if err != nil {
			t.Errorf("detectLocalIPs(any) error = %v", err)
		}
		// Should return at least one non-loopback IP or empty if none available
		for _, ip := range ips {
			if ip == "127.0.0.1" || ip == "::1" {
				t.Errorf("detectLocalIPs(any) should exclude loopback, got %s", ip)
			}
		}
	})

	t.Run("loopback_excluded", func(t *testing.T) {
		ips, err := c.detectLocalIPs("any")
		if err != nil {
			t.Skipf("Skipping loopback test: %v", err)
		}
		// Verify no loopback addresses are included
		for _, ip := range ips {
			if ip == "127.0.0.1" {
				t.Error("detectLocalIPs should exclude 127.0.0.1")
			}
		}
	})
}

func TestCaptureImpl_detectLocalIPs_SpecificInterface(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	// Test with a non-existent interface
	_, err := c.detectLocalIPs("nonexistent_interface_xyz")
	if err == nil {
		t.Error("detectLocalIPs with non-existent interface should return error")
	}
}

func TestCaptureImpl_extractIP(t *testing.T) {
	tests := []struct {
		name     string
		addr     net.Addr
		wantIPv4 bool
		wantIPv6 bool
	}{
		{
			name: "IPv4",
			addr: &net.IPNet{
				IP:   net.ParseIP("192.168.1.10"),
				Mask: net.CIDRMask(24, 32),
			},
			wantIPv4: true,
		},
		{
			name: "IPv6",
			addr: &net.IPNet{
				IP:   net.ParseIP("2001:db8::1"),
				Mask: net.CIDRMask(64, 128),
			},
			wantIPv6: true,
		},
		{
			name:     "nil",
			addr:     nil,
			wantIPv4: false,
			wantIPv6: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := extractIP(tt.addr)
			if tt.wantIPv4 {
				if got == nil || got.To4() == nil {
					t.Error("extractIP() should return IPv4 address")
				}
			}
			if tt.wantIPv6 {
				if got == nil || got.To4() != nil {
					t.Error("extractIP() should return IPv6 address")
				}
			}
			if !tt.wantIPv4 && !tt.wantIPv6 {
				if got != nil {
					t.Error("extractIP() should return nil for nil address")
				}
			}
		})
	}
}

func TestCaptureImpl_buildBPFFilter(t *testing.T) {
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}

	tests := []struct {
		name      string
		ports     []uint16
		localIPs  []string
		wantParts []string // Parts that should be in the filter
	}{
		{
			name:      "no ports",
			ports:     []uint16{},
			localIPs:  []string{},
			wantParts: []string{"tcp"},
		},
		{
			name:      "single port no IPs",
			ports:     []uint16{443},
			localIPs:  []string{},
			wantParts: []string{"tcp dst port 443"},
		},
		{
			name:      "single port with single IP",
			ports:     []uint16{443},
			localIPs:  []string{"192.168.1.10"},
			wantParts: []string{"tcp dst port 443", "dst host 192.168.1.10"},
		},
		{
			name:      "multiple ports with multiple IPs",
			ports:     []uint16{443, 8443},
			localIPs:  []string{"192.168.1.10", "10.0.0.5"},
			wantParts: []string{"tcp dst port 443", "tcp dst port 8443", "dst host 192.168.1.10", "dst host 10.0.0.5"},
		},
		{
			name:      "IPv6 address",
			ports:     []uint16{443},
			localIPs:  []string{"2001:db8::1"},
			wantParts: []string{"tcp dst port 443", "dst host 2001:db8::1"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := c.buildBPFFilter(tt.ports, tt.localIPs)
			for _, part := range tt.wantParts {
				if !strings.Contains(got, part) {
					t.Errorf("buildBPFFilter() = %q, should contain %q", got, part)
				}
			}
		})
	}
}

func TestCaptureImpl_Run_AnyInterface(t *testing.T) {
	t.Skip("integration: pcap on 'any' interface blocks until close; run with -run=Integration in a real network env")
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}
	cfg := api.Config{
		Interface:   "any",
		ListenPorts: []uint16{443},
		LocalIPs:    []string{"192.168.1.10"},
	}
	out := make(chan api.RawPacket, 10)
	errCh := make(chan error, 1)
	go func() { errCh <- c.Run(cfg, out) }()
	// Allow up to 300ms for the handle to open (or fail immediately)
	select {
	case err := <-errCh:
		// Immediate error: permission or "not found"
		if err != nil && strings.Contains(err.Error(), "not found") {
			t.Errorf("Run() with 'any' interface should be valid, got: %v", err)
		}
	case <-time.After(300 * time.Millisecond):
		// Run() started successfully (blocking on packets) — close to stop it
		c.Close()
	}
}

func TestCaptureImpl_Run_WithManualLocalIPs(t *testing.T) {
	t.Skip("integration: pcap on 'any' interface blocks until close; run with -run=Integration in a real network env")
	c := New()
	if c == nil {
		t.Fatal("New() returned nil")
	}
	cfg := api.Config{
		Interface:   "any",
		ListenPorts: []uint16{443},
		LocalIPs:    []string{"192.168.1.10", "10.0.0.5"},
	}
	out := make(chan api.RawPacket, 10)
	errCh := make(chan error, 1)
	go func() { errCh <- c.Run(cfg, out) }()
	select {
	case err := <-errCh:
		if err != nil && strings.Contains(err.Error(), "not found") {
			t.Errorf("Run() with manual LocalIPs should be valid, got: %v", err)
		}
	case <-time.After(300 * time.Millisecond):
		c.Close()
	}
}

// TestCaptureImpl_LinkTypeInitializedOnce verifies that linkType is set exactly once,
// after the BPF filter is applied (Bug 2 fix: removed the redundant early assignment).
func TestCaptureImpl_LinkTypeInitializedOnce(t *testing.T) {
	c := New()
	// Fresh instance: linkType must be zero before Run() is called.
	if c.linkType != 0 {
		t.Errorf("new CaptureImpl should have linkType=0, got %d", c.linkType)
	}

	// GetDiagnostics reflects linkType correctly.
	_, _, _, lt := c.GetDiagnostics()
	if lt != 0 {
		t.Errorf("GetDiagnostics() linkType before Run() should be 0, got %d", lt)
	}

	// Simulate what Run() does: set linkType once under the mutex.
	c.mu.Lock()
	c.linkType = 1 // 1 = Ethernet
	c.mu.Unlock()

	_, _, _, lt = c.GetDiagnostics()
	if lt != 1 {
		t.Errorf("GetDiagnostics() linkType after set = %d, want 1", lt)
	}
}

// TestBuildBPFFilter_NoLocalIPs verifies Bug 3 fix: when no local IPs are
// available (NAT/VIP), buildBPFFilter returns a port-only filter.
func TestBuildBPFFilter_NoLocalIPs(t *testing.T) {
c := New()
filter := c.buildBPFFilter([]uint16{443}, nil)
if strings.Contains(filter, "dst host") {
t.Errorf("port-only filter expected when localIPs nil, got: %s", filter)
}
if !strings.Contains(filter, "tcp dst port 443") {
t.Errorf("expected tcp dst port 443, got: %s", filter)
}
}

func TestBuildBPFFilter_EmptyLocalIPs(t *testing.T) {
c := New()
filter := c.buildBPFFilter([]uint16{443, 8443}, []string{})
if strings.Contains(filter, "dst host") {
t.Errorf("port-only filter expected when localIPs empty, got: %s", filter)
}
if !strings.Contains(filter, "tcp dst port 443") || !strings.Contains(filter, "tcp dst port 8443") {
t.Errorf("expected both ports in filter, got: %s", filter)
}
}
