package routing

import (
	"context"
	"errors"
	"path/filepath"
	"testing"
	"time"

	"sub2api-cn-relay-manager/internal/store/sqlite"
)

func TestSQLiteLogWriterAppendAndFlush(t *testing.T) {
	t.Parallel()

	dbPath := filepath.Join(t.TempDir(), "route-logs.db")
	dsn := "file:" + filepath.ToSlash(dbPath) + "?_busy_timeout=5000"
	writer, err := NewSQLiteLogWriter(context.Background(), dsn, AsyncLogWriterOptions{
		QueueSize:     4,
		FlushInterval: time.Hour,
		MaxBatchSize:  8,
	})
	if err != nil {
		t.Fatalf("NewSQLiteLogWriter() error = %v", err)
	}
	defer writer.Close()

	if err := writer.AppendDecision(context.Background(), RouteDecisionEvent{
		RequestID:             "req-1",
		LogicalGroupID:        "gpt-shared",
		PublicModel:           "gpt-5.4",
		StickyKey:             "sticky-1",
		StickyKeyType:         "conversation",
		StickyHit:             true,
		SelectedRouteID:       "asxs",
		SelectedShadowGroupID: "gpt-shared__asxs",
		UpstreamStatus:        200,
		LatencyMS:             120,
	}); err != nil {
		t.Fatalf("AppendDecision() error = %v", err)
	}
	if err := writer.AppendFailover(context.Background(), RouteFailoverEvent{
		RequestID:      "req-2",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		FromRouteID:    "asxs",
		ToRouteID:      "codex2api",
		Reason:         "timeout",
		FailureCount:   2,
	}); err != nil {
		t.Fatalf("AppendFailover() error = %v", err)
	}
	if err := writer.AppendStickyAudit(context.Background(), RouteStickyAuditEvent{
		StickyKey:      "sticky-1",
		StickyKeyType:  "conversation",
		LogicalGroupID: "gpt-shared",
		PublicModel:    "gpt-5.4",
		RouteID:        "asxs",
		Action:         "bind",
		ExpiresAt:      "2026-05-28T18:00:00Z",
	}); err != nil {
		t.Fatalf("AppendStickyAudit() error = %v", err)
	}
	if err := writer.Flush(context.Background()); err != nil {
		t.Fatalf("Flush() error = %v", err)
	}

	store, err := sqlite.Open(context.Background(), dsn)
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	decisions, err := store.RouteDecisionLogs().ListRecent(context.Background(), sqlite.RouteDecisionLogFilter{Limit: 10})
	if err != nil {
		t.Fatalf("RouteDecisionLogs().ListRecent() error = %v", err)
	}
	if len(decisions) != 1 || decisions[0].SelectedRouteID != "asxs" || !decisions[0].StickyHit {
		t.Fatalf("decision logs = %+v", decisions)
	}

	failovers, err := store.RouteFailoverEvents().ListRecent(context.Background(), sqlite.RouteFailoverEventFilter{Limit: 10})
	if err != nil {
		t.Fatalf("RouteFailoverEvents().ListRecent() error = %v", err)
	}
	if len(failovers) != 1 || failovers[0].ToRouteID != "codex2api" {
		t.Fatalf("failover events = %+v", failovers)
	}

	stickyAudit, err := store.RouteStickyAudit().ListRecent(context.Background(), sqlite.RouteStickyAuditFilter{Limit: 10})
	if err != nil {
		t.Fatalf("RouteStickyAudit().ListRecent() error = %v", err)
	}
	if len(stickyAudit) != 1 || stickyAudit[0].Action != "bind" {
		t.Fatalf("sticky audit = %+v", stickyAudit)
	}
}

func TestAsyncLogWriterFlushFailureDoesNotCrash(t *testing.T) {
	t.Parallel()

	sink := &failingRouteLogSink{}
	writer := NewAsyncLogWriter(sink, AsyncLogWriterOptions{
		QueueSize:     2,
		FlushInterval: time.Hour,
		MaxBatchSize:  2,
	})
	defer writer.Close()

	if err := writer.AppendDecision(context.Background(), RouteDecisionEvent{
		RequestID:             "req-fail",
		LogicalGroupID:        "gpt-shared",
		PublicModel:           "gpt-5.4",
		SelectedRouteID:       "asxs",
		SelectedShadowGroupID: "gpt-shared__asxs",
	}); err != nil {
		t.Fatalf("AppendDecision() error = %v", err)
	}
	if err := writer.Flush(context.Background()); err == nil {
		t.Fatal("Flush() error = nil, want failure")
	}
	if sink.appendCalls == 0 {
		t.Fatal("appendCalls = 0, want at least one attempted write")
	}
}

func TestAsyncLogWriterFlushesQueuedEvents(t *testing.T) {
	t.Parallel()

	sink := &recordingRouteLogSink{}
	writer := NewAsyncLogWriter(sink, AsyncLogWriterOptions{
		QueueSize:     1,
		FlushInterval: time.Hour,
		MaxBatchSize:  100,
	})
	defer writer.Close()

	if err := writer.AppendDecision(context.Background(), RouteDecisionEvent{
		RequestID:             "req-a",
		LogicalGroupID:        "gpt-shared",
		PublicModel:           "gpt-5.4",
		SelectedRouteID:       "asxs",
		SelectedShadowGroupID: "gpt-shared__asxs",
	}); err != nil {
		t.Fatalf("AppendDecision() first error = %v", err)
	}
	if err := writer.AppendDecision(context.Background(), RouteDecisionEvent{
		RequestID:             "req-b",
		LogicalGroupID:        "gpt-shared",
		PublicModel:           "gpt-5.4",
		SelectedRouteID:       "codex2api",
		SelectedShadowGroupID: "gpt-shared__codex2api",
	}); err != nil {
		t.Fatalf("AppendDecision() second error = %v", err)
	}
	if err := writer.Flush(context.Background()); err != nil {
		t.Fatalf("Flush() error = %v", err)
	}

	if len(sink.decisions) != 2 {
		t.Fatalf("recorded decisions len = %d, want 2", len(sink.decisions))
	}
}

type failingRouteLogSink struct {
	appendCalls int
}

func (s *failingRouteLogSink) AppendDecision(context.Context, RouteDecisionEvent) error {
	s.appendCalls++
	return errors.New("boom")
}

func (s *failingRouteLogSink) AppendFailover(context.Context, RouteFailoverEvent) error {
	s.appendCalls++
	return errors.New("boom")
}

func (s *failingRouteLogSink) AppendStickyAudit(context.Context, RouteStickyAuditEvent) error {
	s.appendCalls++
	return errors.New("boom")
}

func (s *failingRouteLogSink) Close() error { return nil }

type recordingRouteLogSink struct {
	decisions []RouteDecisionEvent
}

func (s *recordingRouteLogSink) AppendDecision(_ context.Context, event RouteDecisionEvent) error {
	s.decisions = append(s.decisions, event)
	return nil
}

func (s *recordingRouteLogSink) AppendFailover(context.Context, RouteFailoverEvent) error {
	return nil
}

func (s *recordingRouteLogSink) AppendStickyAudit(context.Context, RouteStickyAuditEvent) error {
	return nil
}

func (s *recordingRouteLogSink) Close() error { return nil }
