package performance

import (
	"context"
	"database/sql"
	"path/filepath"
	"testing"
	"testing/fstest"
	"time"

	"github.com/rs/zerolog"

	dbpkg "github.com/operator/command-center/internal/db"
	"github.com/operator/command-center/internal/eventbus"
	"github.com/operator/command-center/internal/webhooks"
)

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

	migs, err := dbpkg.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 torrents (info_hash TEXT PRIMARY KEY, name TEXT NOT NULL, size_bytes INTEGER NOT NULL, category TEXT, tags TEXT, first_seen_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, source_filter_id TEXT, source_tracker_id TEXT, cross_seed_origin_hash TEXT, deleted_at INTEGER);
		`)},
		"004_phase3.sql": {Data: []byte(`
			CREATE TABLE torrent_clients (id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, base_url TEXT NOT NULL, proxy_via_qui INTEGER NOT NULL DEFAULT 0, config_json TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, poll_interval_seconds INTEGER NOT NULL DEFAULT 30, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
			CREATE TABLE torrent_snapshots (id INTEGER PRIMARY KEY AUTOINCREMENT, info_hash TEXT NOT NULL, client_id TEXT NOT NULL, timestamp INTEGER NOT NULL, simulation_id INTEGER, uploaded_bytes INTEGER, downloaded_bytes INTEGER, state TEXT, ratio REAL, seeders INTEGER, leechers INTEGER, upload_speed_bps INTEGER, download_speed_bps INTEGER);
		`)},
		"006_phase5.sql": {Data: []byte(`
			CREATE TABLE automation_tools (id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, base_url TEXT NOT NULL, config_json TEXT NOT NULL, webhook_token TEXT, enabled INTEGER NOT NULL DEFAULT 1, poll_interval_seconds INTEGER NOT NULL DEFAULT 60, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
			CREATE TABLE filter_performance (id INTEGER PRIMARY KEY AUTOINCREMENT, automation_tool_id TEXT NOT NULL, filter_external_id TEXT NOT NULL, filter_name TEXT NOT NULL, variant_id TEXT, info_hash TEXT, release_name TEXT, grabbed_at INTEGER NOT NULL, final_uploaded_bytes INTEGER, final_ratio REAL, last_measured_at INTEGER);
		`)},
	})
	if err != nil {
		t.Fatalf("LoadMigrations: %v", err)
	}
	if _, err := dbpkg.Apply(db, migs); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	_, _ = db.Exec(`INSERT INTO automation_tools VALUES ('autobrr', 'autobrr', 'autobrr', 'http://x', '{}', NULL, 1, 60, 0, 0)`)
	_, _ = db.Exec(`INSERT INTO torrent_clients VALUES ('qbit', 'qbit', 'qbit', 'http://x', 0, '{}', 1, 30, 0, 0)`)
	return db
}

func TestLinkerRecordsGrabWithHash(t *testing.T) {
	db := openPerformanceDB(t)
	bus := eventbus.New(zerolog.Nop())
	l := NewLinker(db, bus, zerolog.Nop())

	grab := webhooks.AutobrrGrabEvent{
		EventID:     "ev-1",
		FilterID:    "42",
		FilterName:  "1080p Internal",
		ReleaseName: "Some.Release.2024",
		InfoHash:    "abc123",
		GrabbedAt:   time.Now(),
		EndpointID:  "autobrr",
	}
	if err := l.RecordGrab(context.Background(), grab); err != nil {
		t.Fatalf("RecordGrab: %v", err)
	}
	var count int
	var hash sql.NullString
	row := db.QueryRow(`SELECT COUNT(*), MAX(info_hash) FROM filter_performance`)
	if err := row.Scan(&count, &hash); err != nil {
		t.Fatalf("scan: %v", err)
	}
	if count != 1 || !hash.Valid || hash.String != "abc123" {
		t.Errorf("got count=%d hash=%v", count, hash)
	}
}

func TestLinkerRecordsGrabUnlinked(t *testing.T) {
	db := openPerformanceDB(t)
	bus := eventbus.New(zerolog.Nop())
	l := NewLinker(db, bus, zerolog.Nop())

	grab := webhooks.AutobrrGrabEvent{
		FilterID:    "42",
		FilterName:  "1080p",
		ReleaseName: "Some.Release",
		GrabbedAt:   time.Now(),
		EndpointID:  "autobrr",
	}
	if err := l.RecordGrab(context.Background(), grab); err != nil {
		t.Fatalf("RecordGrab: %v", err)
	}
	var hash sql.NullString
	_ = db.QueryRow(`SELECT info_hash FROM filter_performance`).Scan(&hash)
	if hash.Valid {
		t.Errorf("expected NULL info_hash, got %q", hash.String)
	}
}

func TestLinkerSubscribesAndProcesses(t *testing.T) {
	db := openPerformanceDB(t)
	bus := eventbus.New(zerolog.Nop())
	l := NewLinker(db, bus, zerolog.Nop())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	l.Start(ctx)
	defer l.Stop()

	// Wait for the worker goroutine to register its subscription before
	// publishing; otherwise Publish can fire before Subscribe and the
	// at-most-once eventbus drops the event.
	deadline := time.Now().Add(2 * time.Second)
	for bus.SubscriberCount(eventbus.TopicAutobrrGrab) == 0 && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}

	bus.Publish(eventbus.Event{
		Topic: eventbus.TopicAutobrrGrab,
		Payload: webhooks.AutobrrGrabEvent{
			FilterID: "1", FilterName: "A", ReleaseName: "R", InfoHash: "h",
			GrabbedAt: time.Now(), EndpointID: "autobrr",
		},
	})
	deadline = time.Now().Add(2 * time.Second)
	var count int
	for time.Now().Before(deadline) && count == 0 {
		_ = db.QueryRow(`SELECT COUNT(*) FROM filter_performance`).Scan(&count)
		time.Sleep(20 * time.Millisecond)
	}
	if count != 1 {
		t.Errorf("expected 1 row, got %d", count)
	}
}

func TestScorerLinksByFuzzyName(t *testing.T) {
	db := openPerformanceDB(t)
	s := NewScorer(db, zerolog.Nop())

	now := time.Now().Unix()
	_, err := db.Exec(`INSERT INTO filter_performance
		(automation_tool_id, filter_external_id, filter_name, release_name, grabbed_at)
		VALUES ('autobrr', '42', '1080p', 'Some.Release.2024.1080p.WEB.DL', ?)`, now)
	if err != nil {
		t.Fatalf("insert grab: %v", err)
	}
	_, err = db.Exec(`INSERT INTO torrents
		(info_hash, name, size_bytes, first_seen_at, last_seen_at)
		VALUES ('h1', 'Some.Release.2024.1080p.WEB.DL-GROUP', 1000, ?, ?)`, now+10, now+10)
	if err != nil {
		t.Fatalf("insert torrent: %v", err)
	}

	if err := s.Reconcile(context.Background()); err != nil {
		t.Fatalf("Reconcile: %v", err)
	}
	var hash sql.NullString
	_ = db.QueryRow(`SELECT info_hash FROM filter_performance`).Scan(&hash)
	if !hash.Valid || hash.String != "h1" {
		t.Errorf("expected info_hash=h1, got %v", hash)
	}
}

func TestScorerUpdatesFinalUploadedFromSnapshots(t *testing.T) {
	db := openPerformanceDB(t)
	s := NewScorer(db, zerolog.Nop())

	now := time.Now().Unix()
	_, _ = db.Exec(`INSERT INTO filter_performance
		(automation_tool_id, filter_external_id, filter_name, info_hash, release_name, grabbed_at)
		VALUES ('autobrr', '42', '1080p', 'h2', 'R', ?)`, now-3600)
	_, _ = db.Exec(`INSERT INTO torrents (info_hash, name, size_bytes, first_seen_at, last_seen_at)
		VALUES ('h2', 'R', 0, ?, ?)`, now-3000, now)
	for i := 0; i < 3; i++ {
		_, _ = db.Exec(`INSERT INTO torrent_snapshots (info_hash, client_id, timestamp, uploaded_bytes, ratio)
			VALUES ('h2', 'qbit', ?, ?, ?)`, now-2000+int64(i*500), int64(1000+i*1000), 1.5+float64(i)*0.5)
	}

	if err := s.Reconcile(context.Background()); err != nil {
		t.Fatalf("Reconcile: %v", err)
	}
	var up sql.NullInt64
	var ratio sql.NullFloat64
	_ = db.QueryRow(`SELECT final_uploaded_bytes, final_ratio FROM filter_performance WHERE info_hash = ?`, "h2").Scan(&up, &ratio)
	if !up.Valid || up.Int64 != 3000 {
		t.Errorf("final_uploaded_bytes: %v, want 3000", up)
	}
	if !ratio.Valid || ratio.Float64 != 2.5 {
		t.Errorf("final_ratio: %v, want 2.5", ratio)
	}
}

func TestScorerDoesntTouchUnlinkedRows(t *testing.T) {
	db := openPerformanceDB(t)
	s := NewScorer(db, zerolog.Nop())
	now := time.Now().Unix()
	_, _ = db.Exec(`INSERT INTO filter_performance
		(automation_tool_id, filter_external_id, filter_name, release_name, grabbed_at)
		VALUES ('autobrr', '42', 'F', 'something-nothing-matches', ?)`, now)
	if err := s.Reconcile(context.Background()); err != nil {
		t.Fatalf("Reconcile: %v", err)
	}
	var hash sql.NullString
	_ = db.QueryRow(`SELECT info_hash FROM filter_performance`).Scan(&hash)
	if hash.Valid {
		t.Errorf("expected unchanged NULL, got %v", hash.String)
	}
}
