package domain

import (
	"encoding/json"
	"testing"
	"time"
)

func TestNormalizedEvent_CorrelationKey(t *testing.T) {
	tests := []struct {
		name     string
		event    *NormalizedEvent
		expected string
	}{
		{
			name: "basic key",
			event: &NormalizedEvent{
				SrcIP:   "192.168.1.1",
				SrcPort: 8080,
			},
			expected: "192.168.1.1:8080",
		},
		{
			name: "different port",
			event: &NormalizedEvent{
				SrcIP:   "10.0.0.1",
				SrcPort: 443,
			},
			expected: "10.0.0.1:443",
		},
		{
			name: "port zero",
			event: &NormalizedEvent{
				SrcIP:   "127.0.0.1",
				SrcPort: 0,
			},
			expected: "127.0.0.1:0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			key := tt.event.CorrelationKey()
			if key != tt.expected {
				t.Errorf("expected %s, got %s", tt.expected, key)
			}
		})
	}
}

func TestNewCorrelatedLogFromEvent(t *testing.T) {
	event := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		DstIP:     "10.0.0.1",
		DstPort:   80,
		Raw: map[string]any{
			"method": "GET",
			"path":   "/api/test",
		},
	}

	log := NewCorrelatedLogFromEvent(event, "A")

	if log.Correlated != 0 {
		t.Error("expected correlated to be 0")
	}
	if log.OrphanSide != "A" {
		t.Errorf("expected orphan_side A, got %s", log.OrphanSide)
	}
	if log.SrcIP != "192.168.1.1" {
		t.Errorf("expected src_ip 192.168.1.1, got %s", log.SrcIP)
	}
	if log.Fields == nil {
		t.Error("expected fields to be non-nil")
	}
}

func TestNewCorrelatedLog(t *testing.T) {
	apacheEvent := &NormalizedEvent{
		Source:    SourceA,
		Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		DstIP:     "10.0.0.1",
		DstPort:   80,
		Raw:       map[string]any{"method": "GET"},
	}

	networkEvent := &NormalizedEvent{
		Source:    SourceB,
		Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 500000000, time.UTC),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		DstIP:     "10.0.0.1",
		DstPort:   80,
		Raw:       map[string]any{"ja3": "abc123"},
	}

	log := NewCorrelatedLog(apacheEvent, networkEvent)

	if log.Correlated != 1 {
		t.Error("expected correlated to be 1")
	}
	if log.OrphanSide != "" {
		t.Errorf("expected orphan_side to be empty, got %s", log.OrphanSide)
	}
	if log.Fields == nil {
		t.Error("expected fields to be non-nil")
	}
}

// TestNewCorrelatedLog_TimestampSelectionAEarlier verifies that when A is earlier the later (B) timestamp is used.
func TestNewCorrelatedLog_TimestampSelectionAEarlier(t *testing.T) {
	tsA := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	tsB := time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC) // B is later

	a := &NormalizedEvent{Source: SourceA, Timestamp: tsA, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}
	b := &NormalizedEvent{Source: SourceB, Timestamp: tsB, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}

	log := NewCorrelatedLog(a, b)
	if !log.Timestamp.Equal(tsB) {
		t.Errorf("expected timestamp to be B's (later), got %v", log.Timestamp)
	}
}

// TestNewCorrelatedLog_TimestampSelectionBEarlier verifies that when B is earlier, A's timestamp is used.
func TestNewCorrelatedLog_TimestampSelectionBEarlier(t *testing.T) {
	tsA := time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC) // A is later
	tsB := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)

	a := &NormalizedEvent{Source: SourceA, Timestamp: tsA, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}
	b := &NormalizedEvent{Source: SourceB, Timestamp: tsB, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}

	log := NewCorrelatedLog(a, b)
	// The later timestamp wins. Since B is not After A, ts stays as A's timestamp.
	if !log.Timestamp.Equal(tsA) {
		t.Errorf("expected timestamp to be A's (later), got %v", log.Timestamp)
	}
}

// TestNewCorrelatedLog_TimestampEqual verifies equal timestamps yield A's timestamp.
func TestNewCorrelatedLog_TimestampEqual(t *testing.T) {
	ts := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
	a := &NormalizedEvent{Source: SourceA, Timestamp: ts, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}
	b := &NormalizedEvent{Source: SourceB, Timestamp: ts, SrcIP: "1.1.1.1", SrcPort: 100, Raw: map[string]any{}}

	log := NewCorrelatedLog(a, b)
	if !log.Timestamp.Equal(ts) {
		t.Errorf("expected timestamp to be equal to both events' timestamp, got %v", log.Timestamp)
	}
}

// TestNewCorrelatedLogFromEvent_WithKeepAlive verifies keepalives field is added when KeepAliveSeq > 0.
func TestNewCorrelatedLogFromEvent_WithKeepAlive(t *testing.T) {
	event := &NormalizedEvent{
		Source:       SourceA,
		Timestamp:    time.Now(),
		SrcIP:        "1.1.1.1",
		SrcPort:      9999,
		KeepAliveSeq: 3,
		Raw:          map[string]any{"method": "GET"},
	}

	log := NewCorrelatedLogFromEvent(event, "A")
	if log.Fields["keepalives"] != 3 {
		t.Errorf("expected keepalives=3, got %v", log.Fields["keepalives"])
	}
}

// TestNewCorrelatedLogFromEvent_NoKeepAlive verifies keepalives field is absent when KeepAliveSeq == 0.
func TestNewCorrelatedLogFromEvent_NoKeepAlive(t *testing.T) {
	event := &NormalizedEvent{
		Source:       SourceA,
		Timestamp:    time.Now(),
		SrcIP:        "1.1.1.1",
		SrcPort:      9999,
		KeepAliveSeq: 0,
		Raw:          map[string]any{"method": "GET"},
	}

	log := NewCorrelatedLogFromEvent(event, "A")
	if _, ok := log.Fields["keepalives"]; ok {
		t.Error("keepalives field should not be present when KeepAliveSeq == 0")
	}
}

// TestMergeFields_NoCollision verifies fields from A and B are merged without conflict.
func TestMergeFields_NoCollision(t *testing.T) {
	a := &NormalizedEvent{Raw: map[string]any{"method": "GET", "path": "/foo"}}
	b := &NormalizedEvent{Raw: map[string]any{"ja4": "abc123", "proto": "TLS"}}

	fields := mergeFields(a, b)
	if fields["method"] != "GET" {
		t.Errorf("expected method=GET, got %v", fields["method"])
	}
	if fields["ja4"] != "abc123" {
		t.Errorf("expected ja4=abc123, got %v", fields["ja4"])
	}
}

// TestMergeFields_SameValueNoPrefix verifies same-value fields are not prefixed.
func TestMergeFields_SameValueNoPrefix(t *testing.T) {
	a := &NormalizedEvent{Raw: map[string]any{"proto": "TCP"}}
	b := &NormalizedEvent{Raw: map[string]any{"proto": "TCP"}}

	fields := mergeFields(a, b)
	if fields["proto"] != "TCP" {
		t.Errorf("expected proto=TCP (no prefix), got %v", fields["proto"])
	}
	if _, ok := fields["a_proto"]; ok {
		t.Error("a_proto should not exist for same-value collision")
	}
	if _, ok := fields["b_proto"]; ok {
		t.Error("b_proto should not exist for same-value collision")
	}
}

// TestMergeFields_DifferentValuePrefix verifies different-value fields get a_/b_ prefix.
func TestMergeFields_DifferentValuePrefix(t *testing.T) {
	a := &NormalizedEvent{Raw: map[string]any{"port": 80}}
	b := &NormalizedEvent{Raw: map[string]any{"port": 443}}

	fields := mergeFields(a, b)
	if fields["a_port"] != 80 {
		t.Errorf("expected a_port=80, got %v", fields["a_port"])
	}
	if fields["b_port"] != 443 {
		t.Errorf("expected b_port=443, got %v", fields["b_port"])
	}
	if _, ok := fields["port"]; ok {
		t.Error("original 'port' key should be removed on collision")
	}
}

// TestCoalesceString_EmptyA tests that when a is empty, b is returned.
func TestCoalesceString_EmptyA(t *testing.T) {
	result := coalesceString("", "fallback")
	if result != "fallback" {
		t.Errorf("expected 'fallback', got %q", result)
	}
}

// TestCoalesceString_NonEmptyA tests that when a is non-empty, a is returned.
func TestCoalesceString_NonEmptyA(t *testing.T) {
	result := coalesceString("primary", "fallback")
	if result != "primary" {
		t.Errorf("expected 'primary', got %q", result)
	}
}

// TestCoalesceInt_ZeroA tests that when a is zero, b is returned.
func TestCoalesceInt_ZeroA(t *testing.T) {
	result := coalesceInt(0, 443)
	if result != 443 {
		t.Errorf("expected 443, got %d", result)
	}
}

// TestCoalesceInt_NonZeroA tests that when a is non-zero, a is returned.
func TestCoalesceInt_NonZeroA(t *testing.T) {
	result := coalesceInt(80, 443)
	if result != 80 {
		t.Errorf("expected 80, got %d", result)
	}
}

// TestMarshalJSON_ReservedKeyProtection verifies reserved keys in Fields are not overwritten.
func TestMarshalJSON_ReservedKeyProtection(t *testing.T) {
	log := CorrelatedLog{
		Timestamp:  time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
		SrcIP:      "1.2.3.4",
		SrcPort:    1234,
		Correlated: 1,
		Fields: map[string]any{
			"src_ip":     "EVIL_OVERRIDE", // should be ignored
			"correlated": false,           // should be ignored
			"extra":      "value",
		},
	}

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

	var flat map[string]any
	if err := json.Unmarshal(data, &flat); err != nil {
		t.Fatalf("Unmarshal failed: %v", err)
	}

	if flat["src_ip"] != "1.2.3.4" {
		t.Errorf("reserved key src_ip should not be overwritten, got %v", flat["src_ip"])
	}
	if flat["correlated"] != float64(1) {
		t.Errorf("reserved key correlated should not be overwritten, got %v", flat["correlated"])
	}
	if flat["extra"] != "value" {
		t.Errorf("non-reserved key extra should be present, got %v", flat["extra"])
	}
}

// TestMarshalJSON_OptionalFieldsOmittedWhenZero verifies DstIP/DstPort are omitted when zero.
func TestMarshalJSON_OptionalFieldsOmittedWhenZero(t *testing.T) {
	log := CorrelatedLog{
		Timestamp:  time.Now(),
		SrcIP:      "1.2.3.4",
		SrcPort:    1234,
		Correlated: 0,
	}

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

	var flat map[string]any
	if err := json.Unmarshal(data, &flat); err != nil {
		t.Fatalf("Unmarshal failed: %v", err)
	}

	if _, ok := flat["dst_ip"]; ok {
		t.Error("dst_ip should be omitted when empty")
	}
	if _, ok := flat["dst_port"]; ok {
		t.Error("dst_port should be omitted when zero")
	}
	if _, ok := flat["orphan_side"]; ok {
		t.Error("orphan_side should be omitted when empty")
	}
}

// TestExtractFields_Basic verifies extractFields copies Raw fields.
func TestExtractFields_Basic(t *testing.T) {
	e := &NormalizedEvent{
		Raw: map[string]any{"key1": "val1", "key2": 42},
	}
	fields := extractFields(e)
	if fields["key1"] != "val1" {
		t.Errorf("expected key1=val1, got %v", fields["key1"])
	}
	if fields["key2"] != 42 {
		t.Errorf("expected key2=42, got %v", fields["key2"])
	}
}

// TestNewCorrelatedLog_KeepAliveSeq verifies keepalives is set from apache event.
func TestNewCorrelatedLog_KeepAliveSeq(t *testing.T) {
	a := &NormalizedEvent{
		Source: SourceA, Timestamp: time.Now(), SrcIP: "1.1.1.1", SrcPort: 100,
		KeepAliveSeq: 5,
		Raw:          map[string]any{},
	}
	b := &NormalizedEvent{
		Source: SourceB, Timestamp: time.Now(), SrcIP: "1.1.1.1", SrcPort: 100,
		Raw: map[string]any{},
	}

	log := NewCorrelatedLog(a, b)
	if log.Fields["keepalives"] != 5 {
		t.Errorf("expected keepalives=5, got %v", log.Fields["keepalives"])
	}
}
