package health

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

	"chat-switchboard/models"
)

// ── Mock Store ──────────────────────────────

type mockStore struct {
	mu      sync.Mutex
	windows map[string]*models.ProviderHealthWindow // keyed by provider_config_id
}

func newMockStore() *mockStore {
	return &mockStore{windows: make(map[string]*models.ProviderHealthWindow)}
}

func (m *mockStore) UpsertWindow(_ context.Context, w *models.ProviderHealthWindow) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	existing, ok := m.windows[w.ProviderConfigID]
	if ok {
		existing.RequestCount += w.RequestCount
		existing.ErrorCount += w.ErrorCount
		existing.TimeoutCount += w.TimeoutCount
		existing.TotalLatencyMs += w.TotalLatencyMs
		if w.MaxLatencyMs > existing.MaxLatencyMs {
			existing.MaxLatencyMs = w.MaxLatencyMs
		}
		if w.LastError != nil {
			existing.LastError = w.LastError
			existing.LastErrorAt = w.LastErrorAt
		}
	} else {
		cp := *w
		m.windows[w.ProviderConfigID] = &cp
	}
	return nil
}

func (m *mockStore) GetCurrentWindow(_ context.Context, id string) (*models.ProviderHealthWindow, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	w, ok := m.windows[id]
	if !ok {
		return nil, nil
	}
	cp := *w
	return &cp, nil
}

func (m *mockStore) ListWindows(_ context.Context, _ string, _ int) ([]models.ProviderHealthWindow, error) {
	return nil, nil
}

func (m *mockStore) ListAllCurrent(_ context.Context) ([]models.ProviderHealthWindow, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var result []models.ProviderHealthWindow
	for _, w := range m.windows {
		result = append(result, *w)
	}
	return result, nil
}

func (m *mockStore) Prune(_ context.Context, _ time.Time) (int64, error) { return 0, nil }

func (m *mockStore) UpsertToolWindow(_ context.Context, _ *models.ToolHealthWindow) error {
	return nil
}

func (m *mockStore) ListAllToolCurrent(_ context.Context) ([]models.ToolHealthWindow, error) {
	return nil, nil
}

// ── Tests ───────────────────────────────────

func TestRecordSuccess(t *testing.T) {
	ms := newMockStore()
	a := &Accumulator{
		buckets: make(map[string]*bucket),
		store:   ms,
		stopCh:  make(chan struct{}),
	}

	a.RecordSuccess("prov-1", 100)
	a.RecordSuccess("prov-1", 200)
	a.RecordSuccess("prov-2", 50)

	a.flush()

	w1, _ := ms.GetCurrentWindow(context.Background(), "prov-1")
	if w1 == nil {
		t.Fatal("expected prov-1 window")
	}
	if w1.RequestCount != 2 {
		t.Fatalf("expected 2 requests, got %d", w1.RequestCount)
	}
	if w1.ErrorCount != 0 {
		t.Fatalf("expected 0 errors, got %d", w1.ErrorCount)
	}
	if w1.TotalLatencyMs != 300 {
		t.Fatalf("expected 300ms total latency, got %d", w1.TotalLatencyMs)
	}
	if w1.MaxLatencyMs != 200 {
		t.Fatalf("expected 200ms max latency, got %d", w1.MaxLatencyMs)
	}

	w2, _ := ms.GetCurrentWindow(context.Background(), "prov-2")
	if w2 == nil {
		t.Fatal("expected prov-2 window")
	}
	if w2.RequestCount != 1 {
		t.Fatalf("expected 1 request, got %d", w2.RequestCount)
	}
}

func TestRecordError(t *testing.T) {
	ms := newMockStore()
	a := &Accumulator{
		buckets: make(map[string]*bucket),
		store:   ms,
		stopCh:  make(chan struct{}),
	}

	a.RecordSuccess("prov-1", 100)
	a.RecordError("prov-1", 500, "connection refused")
	a.RecordSuccess("prov-1", 150)

	a.flush()

	w, _ := ms.GetCurrentWindow(context.Background(), "prov-1")
	if w.RequestCount != 3 {
		t.Fatalf("expected 3 requests, got %d", w.RequestCount)
	}
	if w.ErrorCount != 1 {
		t.Fatalf("expected 1 error, got %d", w.ErrorCount)
	}
	if w.LastError == nil || *w.LastError != "connection refused" {
		t.Fatalf("expected last_error='connection refused', got %v", w.LastError)
	}
}

func TestRecordTimeout(t *testing.T) {
	ms := newMockStore()
	a := &Accumulator{
		buckets: make(map[string]*bucket),
		store:   ms,
		stopCh:  make(chan struct{}),
	}

	a.RecordTimeout("prov-1", 30000, "context deadline exceeded")
	a.flush()

	w, _ := ms.GetCurrentWindow(context.Background(), "prov-1")
	if w.RequestCount != 1 {
		t.Fatalf("expected 1 request, got %d", w.RequestCount)
	}
	if w.ErrorCount != 1 {
		t.Fatalf("expected 1 error, got %d", w.ErrorCount)
	}
	if w.TimeoutCount != 1 {
		t.Fatalf("expected 1 timeout, got %d", w.TimeoutCount)
	}
}

func TestDeriveStatus(t *testing.T) {
	tests := []struct {
		rate     float64
		count    int
		expected models.ProviderStatus
	}{
		{0, 0, models.StatusUnknown},
		{0, 100, models.StatusHealthy},
		{0.03, 100, models.StatusHealthy},
		{0.05, 100, models.StatusDegraded},
		{0.15, 100, models.StatusDegraded},
		{0.25, 100, models.StatusDown},
		{0.50, 100, models.StatusDown},
		{1.0, 1, models.StatusDown},
	}
	for _, tt := range tests {
		got := DeriveStatus(tt.rate, tt.count)
		if got != tt.expected {
			t.Errorf("DeriveStatus(%v, %d) = %s, want %s", tt.rate, tt.count, got, tt.expected)
		}
	}
}

func TestFlushClearsBuckets(t *testing.T) {
	ms := newMockStore()
	a := &Accumulator{
		buckets: make(map[string]*bucket),
		store:   ms,
		stopCh:  make(chan struct{}),
	}

	a.RecordSuccess("prov-1", 100)
	a.flush()

	// After flush, buckets should be empty
	a.mu.Lock()
	count := len(a.buckets)
	a.mu.Unlock()
	if count != 0 {
		t.Fatalf("expected 0 buckets after flush, got %d", count)
	}
}

func TestConcurrentRecording(t *testing.T) {
	ms := newMockStore()
	a := &Accumulator{
		buckets: make(map[string]*bucket),
		store:   ms,
		stopCh:  make(chan struct{}),
	}

	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			a.RecordSuccess("prov-1", 50)
		}()
	}
	wg.Wait()
	a.flush()

	w, _ := ms.GetCurrentWindow(context.Background(), "prov-1")
	if w.RequestCount != 100 {
		t.Fatalf("expected 100 concurrent requests, got %d", w.RequestCount)
	}
}

func TestHourFloor(t *testing.T) {
	ts := time.Date(2026, 3, 1, 14, 37, 42, 0, time.UTC)
	floor := hourFloor(ts)
	expected := time.Date(2026, 3, 1, 14, 0, 0, 0, time.UTC)
	if !floor.Equal(expected) {
		t.Fatalf("expected %v, got %v", expected, floor)
	}
}
