package app

import (
	"context"
	"sync"
	"testing"
	"time"

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

type mockEventSource struct {
	name      string
	mu        sync.RWMutex
	eventChan chan<- *domain.NormalizedEvent
	started   bool
	stopped   bool
}

func (m *mockEventSource) Name() string { return m.name }
func (m *mockEventSource) Start(ctx context.Context, eventChan chan<- *domain.NormalizedEvent) error {
	m.mu.Lock()
	m.started = true
	m.eventChan = eventChan
	m.mu.Unlock()
	<-ctx.Done()
	m.mu.Lock()
	m.stopped = true
	m.mu.Unlock()
	return nil
}
func (m *mockEventSource) Stop() error { return nil }

func (m *mockEventSource) getEventChan() chan<- *domain.NormalizedEvent {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.eventChan
}

func (m *mockEventSource) isStarted() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.started
}

type mockSink struct {
	mu      sync.Mutex
	written []domain.CorrelatedLog
}

func (m *mockSink) Name() string { return "mock" }
func (m *mockSink) Write(ctx context.Context, log domain.CorrelatedLog) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.written = append(m.written, log)
	return nil
}
func (m *mockSink) Flush(ctx context.Context) error { return nil }
func (m *mockSink) Close() error                    { return nil }
func (m *mockSink) Reopen() error                   { return nil }

func (m *mockSink) getWritten() []domain.CorrelatedLog {
	m.mu.Lock()
	defer m.mu.Unlock()
	result := make([]domain.CorrelatedLog, len(m.written))
	copy(result, m.written)
	return result
}

func TestOrchestrator_StartStop(t *testing.T) {
	source := &mockEventSource{name: "test"}
	sink := &mockSink{}

	corrConfig := domain.CorrelationConfig{
		TimeWindow:       time.Second,
		ApacheAlwaysEmit: true,
		NetworkEmit:      false,
	}
	correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

	orchestrator := NewOrchestrator(OrchestratorConfig{
		Sources: []ports.EventSource{source},
		Sink:    sink,
	}, correlationSvc)

	if err := orchestrator.Start(); err != nil {
		t.Fatalf("failed to start: %v", err)
	}

	// Let it run briefly
	time.Sleep(100 * time.Millisecond)

	if err := orchestrator.Stop(); err != nil {
		t.Fatalf("failed to stop: %v", err)
	}

	if !source.isStarted() {
		t.Error("expected source to be started")
	}
}

func TestOrchestrator_ProcessEvent(t *testing.T) {
	source := &mockEventSource{name: "test"}
	sink := &mockSink{}

	corrConfig := domain.CorrelationConfig{
		TimeWindow:       time.Second,
		ApacheAlwaysEmit: true,
		NetworkEmit:      false,
	}
	correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

	orchestrator := NewOrchestrator(OrchestratorConfig{
		Sources: []ports.EventSource{source},
		Sink:    sink,
	}, correlationSvc)

	if err := orchestrator.Start(); err != nil {
		t.Fatalf("failed to start: %v", err)
	}

	// Wait for source to start and get the channel
	var eventChan chan<- *domain.NormalizedEvent
	for i := 0; i < 50; i++ {
		eventChan = source.getEventChan()
		if eventChan != nil {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	if eventChan == nil {
		t.Fatal("source did not start properly")
	}

	// Send an event through the source
	event := &domain.NormalizedEvent{
		Source:    domain.SourceA,
		Timestamp: time.Now(),
		SrcIP:     "192.168.1.1",
		SrcPort:   8080,
		Raw:       map[string]any{"method": "GET"},
	}

	// Send event
	eventChan <- event

	// Give it time to process
	time.Sleep(100 * time.Millisecond)

	if err := orchestrator.Stop(); err != nil {
		t.Fatalf("failed to stop: %v", err)
	}

	// Should have written at least one log (the orphan A)
	written := sink.getWritten()
	if len(written) == 0 {
		t.Error("expected at least one log to be written")
	}
}

// TestOrchestrator_StartTwice tests that calling Start() twice is a no-op (already running).
func TestOrchestrator_StartTwice(t *testing.T) {
source := &mockEventSource{name: "test"}
sink := &mockSink{}

corrConfig := domain.CorrelationConfig{
TimeWindow:       time.Second,
ApacheAlwaysEmit: true,
}
correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

o := NewOrchestrator(OrchestratorConfig{
Sources: []ports.EventSource{source},
Sink:    sink,
}, correlationSvc)

if err := o.Start(); err != nil {
t.Fatalf("first Start() failed: %v", err)
}
if err := o.Start(); err != nil {
t.Errorf("second Start() should be no-op, got: %v", err)
}

o.Stop()
}

// TestOrchestrator_StopTwice tests that calling Stop() twice is a no-op.
func TestOrchestrator_StopTwice(t *testing.T) {
source := &mockEventSource{name: "test"}
sink := &mockSink{}

corrConfig := domain.CorrelationConfig{
TimeWindow:       time.Second,
ApacheAlwaysEmit: true,
}
correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

o := NewOrchestrator(OrchestratorConfig{
Sources: []ports.EventSource{source},
Sink:    sink,
}, correlationSvc)

o.Start()

if err := o.Stop(); err != nil {
t.Errorf("first Stop() failed: %v", err)
}
if err := o.Stop(); err != nil {
t.Errorf("second Stop() should be no-op, got: %v", err)
}
}

// TestOrchestrator_NoSources tests that Orchestrator works with no sources.
func TestOrchestrator_NoSources(t *testing.T) {
sink := &mockSink{}

corrConfig := domain.CorrelationConfig{TimeWindow: time.Second}
correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

o := NewOrchestrator(OrchestratorConfig{
Sources: []ports.EventSource{},
Sink:    sink,
}, correlationSvc)

if err := o.Start(); err != nil {
t.Fatalf("Start() with no sources failed: %v", err)
}

time.Sleep(50 * time.Millisecond)

if err := o.Stop(); err != nil {
t.Errorf("Stop() failed: %v", err)
}
}

// TestOrchestrator_OrphanEmission tests that orphan A events are emitted via tick.
func TestOrchestrator_OrphanEmission(t *testing.T) {
source := &mockEventSource{name: "test"}
sink := &mockSink{}

corrConfig := domain.CorrelationConfig{
TimeWindow:        50 * time.Millisecond,
ApacheAlwaysEmit:  true,
ApacheEmitDelayMs: 10, // Very short delay so orphans emit quickly
}
correlationSvc := domain.NewCorrelationService(corrConfig, &domain.RealTimeProvider{})

o := NewOrchestrator(OrchestratorConfig{
Sources: []ports.EventSource{source},
Sink:    sink,
}, correlationSvc)

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

// Wait for source to be ready
var eventChan chan<- *domain.NormalizedEvent
for i := 0; i < 50; i++ {
eventChan = source.getEventChan()
if eventChan != nil {
break
}
time.Sleep(5 * time.Millisecond)
}
if eventChan == nil {
t.Fatal("source did not start")
}

// Send a source A event (Apache/HTTP)
eventChan <- &domain.NormalizedEvent{
Source:    domain.SourceA,
Timestamp: time.Now(),
SrcIP:     "10.0.0.1",
SrcPort:   12345,
Raw:       map[string]any{"method": "GET"},
}

// Allow time for orphan ticker to fire (OrphanTickInterval = 250ms, but emit delay is 10ms)
time.Sleep(600 * time.Millisecond)

o.Stop()

written := sink.getWritten()
if len(written) == 0 {
t.Error("expected at least one orphan log to be emitted")
}
}

// TestOrchestrator_Constants tests that constants have reasonable values.
func TestOrchestrator_Constants(t *testing.T) {
if DefaultEventChannelBufferSize <= 0 {
t.Error("DefaultEventChannelBufferSize should be positive")
}
if OrphanTickInterval <= 0 {
t.Error("OrphanTickInterval should be positive")
}
}
