package scrape

import (
	"context"
	"database/sql"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"sync/atomic"
	"testing"
	"testing/fstest"
	"time"

	"github.com/rs/zerolog"

	"github.com/operator/command-center/internal/db"
	"github.com/operator/command-center/internal/observability"
)

func setupSchedulerTest(t *testing.T) (*db.SnapshotWriter, *sql.DB) {
	t.Helper()
	dbPath := filepath.Join(t.TempDir(), "test.db")
	sqliteDB, err := db.OpenSQLite(dbPath)
	if err != nil {
		t.Fatalf("OpenSQLite: %v", err)
	}
	t.Cleanup(func() { _ = sqliteDB.Close() })

	// Apply() creates schema_migrations itself; fixture skips it.
	migs, err := db.LoadMigrations(fstest.MapFS{
		"002_phase1.sql": {Data: []byte(`
			CREATE TABLE trackers (
				id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL,
				base_url TEXT NOT NULL, config_json TEXT NOT NULL,
				enabled INTEGER NOT NULL DEFAULT 1,
				scrape_interval_seconds INTEGER NOT NULL DEFAULT 300,
				scrape_jitter_seconds INTEGER NOT NULL DEFAULT 60,
				use_byparr INTEGER NOT NULL DEFAULT 0,
				created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
			);
			CREATE TABLE ratio_snapshots (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				tracker_id TEXT NOT NULL REFERENCES trackers(id),
				timestamp INTEGER NOT NULL,
				simulation_id INTEGER,
				real_uploaded_bytes INTEGER, real_downloaded_bytes INTEGER, real_ratio REAL,
				displayed_uploaded_bytes INTEGER, displayed_downloaded_bytes INTEGER, displayed_ratio REAL,
				bonus_points INTEGER, unsat_count INTEGER, unsat_limit INTEGER,
				class_or_rank TEXT, raw_json TEXT
			);
		`)},
	})
	if err != nil {
		t.Fatalf("LoadMigrations: %v", err)
	}
	if _, err := db.Apply(sqliteDB, migs); err != nil {
		t.Fatalf("Apply: %v", err)
	}

	w := db.NewSnapshotWriter(sqliteDB, nil)
	if err := w.UpsertTracker(context.Background(), db.TrackerRow{
		ID: "t1", Name: "T1", Type: "mam", BaseURL: "https://x",
		ConfigJSON: "{}", Enabled: true,
		ScrapeIntervalSeconds: 1,
	}); err != nil {
		t.Fatalf("UpsertTracker: %v", err)
	}
	return w, sqliteDB
}

// fakeAdapter is a test adapter that returns the same canned RatioSnapshot
// and counts FetchRatio calls.
type fakeAdapter struct {
	id     string
	calls  atomic.Int32
	failOn int32 // if > 0, FetchRatio returns an error every N calls
}

func (f *fakeAdapter) Name() string { return f.id }
func (f *fakeAdapter) FetchRatio(ctx context.Context) (RatioSnapshot, error) {
	c := f.calls.Add(1)
	if f.failOn > 0 && c%f.failOn == 0 {
		return RatioSnapshot{}, http.ErrAbortHandler
	}
	u := int64(c * 100)
	r := 1.5
	return RatioSnapshot{
		RealUploadedBytes:      &u,
		RealRatio:              &r,
		DisplayedUploadedBytes: &u,
		DisplayedRatio:         &r,
		RawJSON:                `{"fake":true}`,
	}, nil
}
func (f *fakeAdapter) Health(ctx context.Context) error { return nil }

func TestSchedulerScrapesOnInterval(t *testing.T) {
	w, sqliteDB := setupSchedulerTest(t)
	adapter := &fakeAdapter{id: "t1"}

	s := NewScheduler(w, observability.NewEventRecorder(nil, zerolog.Nop()), zerolog.Nop())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s.Replace(ctx, []Job{
		{Adapter: adapter, TrackerID: "t1", IntervalSec: 1, JitterSec: 0},
	})

	// Allow time for at least 2 scrapes (initial + one interval).
	time.Sleep(2500 * time.Millisecond)
	s.Stop()

	if calls := adapter.calls.Load(); calls < 2 {
		t.Errorf("expected ≥2 scrapes, got %d", calls)
	}
	var count int
	if err := sqliteDB.QueryRow(`SELECT COUNT(*) FROM ratio_snapshots WHERE tracker_id = ?`, "t1").Scan(&count); err != nil {
		t.Fatalf("count: %v", err)
	}
	if count < 2 {
		t.Errorf("expected ≥2 snapshots, got %d", count)
	}
}

func TestSchedulerManualRefresh(t *testing.T) {
	w, _ := setupSchedulerTest(t)
	adapter := &fakeAdapter{id: "t1"}

	s := NewScheduler(w, nil, zerolog.Nop())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s.Replace(ctx, []Job{
		{Adapter: adapter, TrackerID: "t1", IntervalSec: 3600, JitterSec: 0},
	})
	defer s.Stop()

	// Wait for the initial scrape.
	deadline := time.Now().Add(3 * time.Second)
	for adapter.calls.Load() < 1 && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}
	if adapter.calls.Load() < 1 {
		t.Fatal("initial scrape did not run")
	}
	initial := adapter.calls.Load()

	if err := s.Refresh("t1"); err != nil {
		t.Fatalf("Refresh: %v", err)
	}
	deadline = time.Now().Add(3 * time.Second)
	for adapter.calls.Load() == initial && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}
	if adapter.calls.Load() == initial {
		t.Error("manual refresh did not trigger a scrape")
	}
}

func TestSchedulerRefreshUnknownTracker(t *testing.T) {
	s := NewScheduler(nil, nil, zerolog.Nop())
	if err := s.Refresh("nope"); err == nil {
		t.Fatal("expected error for unknown tracker")
	}
}

func TestSchedulerStopStops(t *testing.T) {
	w, _ := setupSchedulerTest(t)
	adapter := &fakeAdapter{id: "t1"}
	s := NewScheduler(w, nil, zerolog.Nop())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	s.Replace(ctx, []Job{
		{Adapter: adapter, TrackerID: "t1", IntervalSec: 60, JitterSec: 0},
	})
	// Wait for initial scrape.
	deadline := time.Now().Add(3 * time.Second)
	for adapter.calls.Load() < 1 && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}
	s.Stop()
	stoppedAt := adapter.calls.Load()
	time.Sleep(200 * time.Millisecond)
	if adapter.calls.Load() != stoppedAt {
		t.Errorf("scrapes continued after Stop()")
	}
}

// Ensure the httptest server import is still useful (silence unused).
var _ = httptest.NewServer
