package intelligence

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

	"github.com/rs/zerolog"

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

func openIntelligenceDB(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, _ := dbpkg.LoadMigrations(fstest.MapFS{
		"002_phase1.sql": {Data: []byte(`
			CREATE TABLE trackers (id TEXT PRIMARY KEY, name TEXT, type TEXT, base_url TEXT, config_json TEXT, enabled INTEGER, scrape_interval_seconds INTEGER, scrape_jitter_seconds INTEGER, use_byparr INTEGER, created_at INTEGER, updated_at INTEGER);
			CREATE TABLE ratio_snapshots (id INTEGER PRIMARY KEY AUTOINCREMENT, tracker_id TEXT NOT NULL, 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);
			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);
			CREATE TABLE torrent_trackers (id INTEGER PRIMARY KEY AUTOINCREMENT, info_hash TEXT NOT NULL, tracker_id TEXT NOT NULL, seed_time_required_seconds INTEGER, seed_time_accumulated_seconds INTEGER DEFAULT 0, h_and_r_risk_at INTEGER, UNIQUE(info_hash, tracker_id));
		`)},
		"004_phase3.sql": {Data: []byte(`
			CREATE TABLE torrent_clients (id TEXT PRIMARY KEY, name TEXT, type TEXT, base_url TEXT, proxy_via_qui INTEGER, config_json TEXT, enabled INTEGER, poll_interval_seconds INTEGER, created_at INTEGER, updated_at INTEGER);
			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, type TEXT, base_url TEXT, config_json TEXT, webhook_token TEXT, enabled INTEGER, poll_interval_seconds INTEGER, created_at INTEGER, updated_at INTEGER);
			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);
		`)},
		"008_phase7.sql": {Data: []byte(`
			CREATE TABLE decision_log (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL, decision_type TEXT NOT NULL, subject_type TEXT NOT NULL, subject_id TEXT NOT NULL, recommendation TEXT NOT NULL, confidence REAL, provenance_json TEXT NOT NULL, alternatives_json TEXT, operator_action TEXT, operator_action_at INTEGER);
		`)},
	})
	if _, err := dbpkg.Apply(db, migs); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	_, _ = db.Exec(`INSERT INTO trackers VALUES ('mam','MAM','mam','x','{}',1,300,60,0,0,0)`)
	_, _ = db.Exec(`INSERT INTO torrent_clients VALUES ('qbit','qbit','qbit','x',0,'{}',1,30,0,0)`)
	return db
}

func TestRatioVelocityRising(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	// Two points 24 hours apart, ratio went up by 0.5.
	_, _ = db.Exec(`INSERT INTO ratio_snapshots (tracker_id, timestamp, real_ratio) VALUES ('mam', ?, ?)`, now-86400, 1.0)
	_, _ = db.Exec(`INSERT INTO ratio_snapshots (tracker_id, timestamp, real_ratio) VALUES ('mam', ?, ?)`, now, 1.5)

	vs, err := ComputeAll(context.Background(), db, 48*time.Hour)
	if err != nil {
		t.Fatalf("ComputeAll: %v", err)
	}
	if len(vs) != 1 {
		t.Fatalf("got %d, want 1", len(vs))
	}
	if vs[0].Trend != "rising" {
		t.Errorf("trend: %q", vs[0].Trend)
	}
	if vs[0].SlopePerDay < 0.4 || vs[0].SlopePerDay > 0.6 {
		t.Errorf("slope: %.3f", vs[0].SlopePerDay)
	}
}

func TestDeadSwarmsFindIsolatesNoUpload(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	threeDaysAgo := now - 3*86400
	fourDaysAgo := now - 4*86400

	// Torrent A: dead (uploaded_bytes constant over 72h window).
	_, _ = db.Exec(`INSERT INTO torrents VALUES ('h_dead', 'Dead', 100000, NULL, NULL, ?, ?, NULL, 'mam', NULL, NULL)`, fourDaysAgo, now)
	for i := 0; i < 3; i++ {
		_, _ = db.Exec(`INSERT INTO torrent_snapshots (info_hash, client_id, timestamp, uploaded_bytes) VALUES ('h_dead','qbit',?,?)`,
			threeDaysAgo+int64(i*86400), int64(1000))
	}
	// Torrent B: live (uploaded_bytes increases).
	_, _ = db.Exec(`INSERT INTO torrents VALUES ('h_live', 'Live', 200000, NULL, NULL, ?, ?, NULL, 'mam', NULL, NULL)`, fourDaysAgo, now)
	for i := 0; i < 3; i++ {
		_, _ = db.Exec(`INSERT INTO torrent_snapshots (info_hash, client_id, timestamp, uploaded_bytes) VALUES ('h_live','qbit',?,?)`,
			threeDaysAgo+int64(i*86400), int64(1000+i*500))
	}

	ds, err := Find(context.Background(), db, 72)
	if err != nil {
		t.Fatalf("Find: %v", err)
	}
	if len(ds) != 1 || ds[0].InfoHash != "h_dead" {
		t.Errorf("dead swarms: %+v", ds)
	}
}

func TestHRRiskBelowThreshold(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	// Required 1 hour, first_seen 30 minutes ago → 30 min remaining → at risk.
	_, _ = db.Exec(`INSERT INTO torrents VALUES ('h1', 'T', 1, NULL, NULL, ?, ?, NULL, 'mam', NULL, NULL)`, now-1800, now)
	_, _ = db.Exec(`INSERT INTO torrent_trackers (info_hash, tracker_id, seed_time_required_seconds) VALUES ('h1','mam', 3600)`)

	rs, err := FindAtRisk(context.Background(), db, 1.0, 3600)
	if err != nil {
		t.Fatalf("FindAtRisk: %v", err)
	}
	if len(rs) != 1 {
		t.Fatalf("got %d, want 1", len(rs))
	}
	if rs[0].HoursUntilRequirement > 0.6 {
		t.Errorf("hours remaining unexpectedly high: %.2f", rs[0].HoursUntilRequirement)
	}
}

func TestHRRiskAboveThreshold(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	// Required 1 hour, first_seen 10 days ago → already past requirement, no risk.
	_, _ = db.Exec(`INSERT INTO torrents VALUES ('h2', 'T', 1, NULL, NULL, ?, ?, NULL, 'mam', NULL, NULL)`, now-10*86400, now)
	_, _ = db.Exec(`INSERT INTO torrent_trackers (info_hash, tracker_id, seed_time_required_seconds) VALUES ('h2','mam', 3600)`)

	rs, _ := FindAtRisk(context.Background(), db, 1.0, 3600)
	if len(rs) != 0 {
		t.Errorf("expected 0, got %d", len(rs))
	}
}

func TestForecastDeterministicWithSeed(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	// Seed history: 30 grabs over 30 days, 1 GB each → 1 GB/day mean rate.
	for i := 0; i < 30; i++ {
		_, _ = db.Exec(`INSERT INTO torrents VALUES (?, 'G', ?, NULL, NULL, ?, ?, NULL, NULL, NULL, NULL)`,
			"g"+itoa(i), int64(1<<30), now-int64((30-i)*86400), now)
		_, _ = db.Exec(`INSERT INTO filter_performance (automation_tool_id, filter_external_id, filter_name, info_hash, grabbed_at) VALUES ('autobrr', '1', 'F', ?, ?)`,
			"g"+itoa(i), now-int64((30-i)*86400))
	}

	cfg := ForecastConfig{
		HistoryDays:   30,
		Trials:        500,
		HorizonDays:   60,
		CapacityBytes: int64(50) * (1 << 30), // 50 GB
		CurrentBytes:  0,
		Seed:          12345,
	}
	a, err := Forecast(context.Background(), db, cfg)
	if err != nil {
		t.Fatalf("Forecast: %v", err)
	}
	if a.HistorySamples != 30 {
		t.Errorf("history samples: %d, want 30", a.HistorySamples)
	}
	if a.P50DaysToFull == 0 {
		t.Errorf("p50 zero — capacity must be reached in trial horizon")
	}

	// Same seed → same result.
	b, _ := Forecast(context.Background(), db, cfg)
	if a.P50DaysToFull != b.P50DaysToFull {
		t.Errorf("seeded run not deterministic: a=%d b=%d", a.P50DaysToFull, b.P50DaysToFull)
	}
}

func TestRecommendationsEngine(t *testing.T) {
	db := openIntelligenceDB(t)
	now := time.Now().Unix()
	// One dead swarm, no HR risk.
	_, _ = db.Exec(`INSERT INTO torrents VALUES ('h_dead', 'X', 1<<30, NULL, NULL, ?, ?, NULL, 'mam', NULL, NULL)`, now-4*86400, now)
	for i := 0; i < 3; i++ {
		_, _ = db.Exec(`INSERT INTO torrent_snapshots (info_hash, client_id, timestamp, uploaded_bytes) VALUES ('h_dead','qbit',?,?)`,
			now-3*86400+int64(i*86400), int64(1000))
	}

	e := NewEngine(db, zerolog.Nop())
	recs, err := e.Generate(context.Background())
	if err != nil {
		t.Fatalf("Generate: %v", err)
	}
	found := false
	for _, r := range recs {
		if r.Type == "delete_dead_swarms" {
			found = true
			if r.Confidence <= 0 {
				t.Error("delete_dead_swarms confidence not set")
			}
			if r.Provenance.GeneratedAt.IsZero() {
				t.Error("provenance.generated_at empty")
			}
		}
	}
	if !found {
		t.Errorf("delete_dead_swarms recommendation missing; got %+v", recs)
	}
}

// itoa avoids strconv import in the test file.
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var s []byte
	for n > 0 {
		s = append([]byte{byte('0' + n%10)}, s...)
		n /= 10
	}
	return string(s)
}
