package store

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
)

func newTestStore(t *testing.T) *SQLStore {
	t.Helper()
	s, err := NewSQLiteStore(":memory:")
	if err != nil {
		t.Fatalf("NewSQLiteStore: %v", err)
	}
	if err := s.Init(context.Background()); err != nil {
		t.Fatalf("Init: %v", err)
	}
	return s
}

func TestSiteCRUD(t *testing.T) {
	s := newTestStore(t)

	sites, err := s.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	if len(sites) != 0 {
		t.Fatalf("expected 0 sites, got %d", len(sites))
	}

	if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Test", URL: "https://example.com", Type: "http", Interval: 30}); err != nil {
		t.Fatalf("AddSite: %v", err)
	}

	sites, err = s.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	if len(sites) != 1 {
		t.Fatalf("expected 1 site, got %d", len(sites))
	}
	if sites[0].Name != "Test" {
		t.Errorf("expected name 'Test', got '%s'", sites[0].Name)
	}

	sites[0].Name = "Updated"
	if err := s.UpdateSite(context.Background(), sites[0]); err != nil {
		t.Fatalf("UpdateSite: %v", err)
	}

	sites, err = s.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	if sites[0].Name != "Updated" {
		t.Errorf("expected name 'Updated', got '%s'", sites[0].Name)
	}

	if err := s.DeleteSite(context.Background(), sites[0].ID); err != nil {
		t.Fatalf("DeleteSite: %v", err)
	}

	sites, err = s.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	if len(sites) != 0 {
		t.Fatalf("expected 0 sites after delete, got %d", len(sites))
	}
}

func TestAlertCRUD(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com/hook"}); err != nil {
		t.Fatalf("AddAlert: %v", err)
	}

	alerts, err := s.GetAllAlerts(context.Background())
	if err != nil {
		t.Fatalf("GetAllAlerts: %v", err)
	}
	if len(alerts) != 1 {
		t.Fatalf("expected 1 alert, got %d", len(alerts))
	}
	if alerts[0].Type != "discord" {
		t.Errorf("expected type 'discord', got '%s'", alerts[0].Type)
	}
	if alerts[0].Settings["url"] != "https://example.com/hook" {
		t.Errorf("settings url mismatch")
	}

	a, err := s.GetAlert(context.Background(), alerts[0].ID)
	if err != nil {
		t.Fatalf("GetAlert: %v", err)
	}
	if a.Name != "Discord" {
		t.Errorf("expected name 'Discord', got '%s'", a.Name)
	}

	if err := s.UpdateAlert(context.Background(), a.ID, "Slack", "slack", map[string]string{"url": "https://slack.com/hook"}); err != nil {
		t.Fatalf("UpdateAlert: %v", err)
	}

	a, err = s.GetAlert(context.Background(), a.ID)
	if err != nil {
		t.Fatalf("GetAlert: %v", err)
	}
	if a.Type != "slack" {
		t.Errorf("expected type 'slack', got '%s'", a.Type)
	}

	if err := s.DeleteAlert(context.Background(), a.ID); err != nil {
		t.Fatalf("DeleteAlert: %v", err)
	}

	alerts, err = s.GetAllAlerts(context.Background())
	if err != nil {
		t.Fatalf("GetAllAlerts: %v", err)
	}
	if len(alerts) != 0 {
		t.Fatalf("expected 0 alerts after delete, got %d", len(alerts))
	}
}

func TestUserCRUD(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddUser(context.Background(), "admin", "ssh-ed25519 AAAA...", "admin"); err != nil {
		t.Fatalf("AddUser: %v", err)
	}

	users, err := s.GetAllUsers(context.Background())
	if err != nil {
		t.Fatalf("GetAllUsers: %v", err)
	}
	if len(users) != 1 {
		t.Fatalf("expected 1 user, got %d", len(users))
	}
	if users[0].Username != "admin" {
		t.Errorf("expected username 'admin', got '%s'", users[0].Username)
	}

	if err := s.UpdateUser(context.Background(), users[0].ID, "root", "ssh-ed25519 BBBB...", "admin"); err != nil {
		t.Fatalf("UpdateUser: %v", err)
	}

	users, err = s.GetAllUsers(context.Background())
	if err != nil {
		t.Fatalf("GetAllUsers: %v", err)
	}
	if users[0].Username != "root" {
		t.Errorf("expected username 'root', got '%s'", users[0].Username)
	}

	if err := s.DeleteUser(context.Background(), users[0].ID); err != nil {
		t.Fatalf("DeleteUser: %v", err)
	}

	users, err = s.GetAllUsers(context.Background())
	if err != nil {
		t.Fatalf("GetAllUsers: %v", err)
	}
	if len(users) != 0 {
		t.Fatalf("expected 0 users after delete, got %d", len(users))
	}
}

func TestPushTokenGeneration(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Push Monitor", Type: "push", Interval: 60}); err != nil {
		t.Fatalf("AddSite: %v", err)
	}

	sites, err := s.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	if len(sites) != 1 {
		t.Fatalf("expected 1 site, got %d", len(sites))
	}
	if sites[0].Token == "" {
		t.Error("expected non-empty token for push monitor")
	}
	if len(sites[0].Token) != 32 {
		t.Errorf("expected 32-char hex token, got %d chars", len(sites[0].Token))
	}
}

func TestImportExport(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddAlert(context.Background(), "Test Alert", "webhook", map[string]string{"url": "https://example.com"}); err != nil {
		t.Fatalf("AddAlert: %v", err)
	}
	if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Site1", URL: "https://example.com", Type: "http", Interval: 30}); err != nil {
		t.Fatalf("AddSite: %v", err)
	}
	if err := s.AddUser(context.Background(), "user1", "ssh-ed25519 KEY", "user"); err != nil {
		t.Fatalf("AddUser: %v", err)
	}

	backup, err := s.ExportData(context.Background())
	if err != nil {
		t.Fatalf("ExportData: %v", err)
	}
	if len(backup.Sites) != 1 || len(backup.Alerts) != 1 || len(backup.Users) != 1 {
		t.Fatalf("export mismatch: %d sites, %d alerts, %d users", len(backup.Sites), len(backup.Alerts), len(backup.Users))
	}

	s2 := newTestStore(t)
	if err := s2.ImportData(context.Background(), backup); err != nil {
		t.Fatalf("ImportData: %v", err)
	}

	sites, err := s2.GetSites(context.Background())
	if err != nil {
		t.Fatalf("GetSites: %v", err)
	}
	alerts, err := s2.GetAllAlerts(context.Background())
	if err != nil {
		t.Fatalf("GetAllAlerts: %v", err)
	}
	users, err := s2.GetAllUsers(context.Background())
	if err != nil {
		t.Fatalf("GetAllUsers: %v", err)
	}
	if len(sites) != 1 || len(alerts) != 1 || len(users) != 1 {
		t.Fatalf("import mismatch: %d sites, %d alerts, %d users", len(sites), len(alerts), len(users))
	}
}

func TestImportData_WipesHistory(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddSite(context.Background(), models.SiteConfig{Name: "OldSite", URL: "https://old.com", Type: "http", Interval: 30}); err != nil {
		t.Fatalf("AddSite: %v", err)
	}
	if err := s.SaveCheck(context.Background(), 1, 5000, true); err != nil {
		t.Fatalf("SaveCheck: %v", err)
	}
	if err := s.SaveStateChange(context.Background(), 1, "UP", "DOWN", "timeout"); err != nil {
		t.Fatalf("SaveStateChange: %v", err)
	}
	if err := s.SaveAlertHealth(context.Background(), models.AlertHealthRecord{AlertID: 1, LastSendOK: true, SendCount: 1}); err != nil {
		t.Fatalf("SaveAlertHealth: %v", err)
	}

	backup := models.Backup{
		Sites: []models.SiteConfig{{ID: 1, Name: "NewSite", URL: "https://new.com", Type: "http", Interval: 60}},
	}
	if err := s.ImportData(context.Background(), backup); err != nil {
		t.Fatalf("ImportData: %v", err)
	}

	history, err := s.LoadAllHistory(context.Background(), 100)
	if err != nil {
		t.Fatalf("LoadAllHistory: %v", err)
	}
	if len(history) != 0 {
		t.Errorf("expected empty check_history after import, got %d sites with history", len(history))
	}

	changes, err := s.GetStateChanges(context.Background(), 1, 100)
	if err != nil {
		t.Fatalf("GetStateChanges: %v", err)
	}
	if len(changes) != 0 {
		t.Errorf("expected empty state_changes after import, got %d", len(changes))
	}
}

func TestImportData_NilUsersPreservesExisting(t *testing.T) {
	s := newTestStore(t)

	if err := s.AddUser(context.Background(), "admin", "ssh-ed25519 ADMINKEY", "admin"); err != nil {
		t.Fatalf("AddUser: %v", err)
	}

	backup := models.Backup{
		Sites:  []models.SiteConfig{{ID: 1, Name: "New", URL: "https://new.com", Type: "http", Interval: 30}},
		Alerts: []models.AlertConfig{{ID: 1, Name: "a", Type: "webhook", Settings: map[string]string{"url": "https://h.com"}}},
		Users:  nil,
	}
	if err := s.ImportData(context.Background(), backup); err != nil {
		t.Fatalf("ImportData: %v", err)
	}

	users, err := s.GetAllUsers(context.Background())
	if err != nil {
		t.Fatalf("GetAllUsers: %v", err)
	}
	if len(users) != 1 || users[0].Username != "admin" {
		t.Errorf("expected existing admin user preserved, got %d users", len(users))
	}
}

func TestCheckHistory(t *testing.T) {
	s := newTestStore(t)

	if err := s.SaveCheck(context.Background(), 1, 5000000, true); err != nil {
		t.Fatalf("SaveCheck: %v", err)
	}
	if err := s.SaveCheck(context.Background(), 1, 10000000, false); err != nil {
		t.Fatalf("SaveCheck: %v", err)
	}
	if err := s.SaveCheck(context.Background(), 2, 3000000, true); err != nil {
		t.Fatalf("SaveCheck site 2: %v", err)
	}

	history, err := s.LoadAllHistory(context.Background(), 10)
	if err != nil {
		t.Fatalf("LoadAllHistory: %v", err)
	}
	if len(history[1]) != 2 {
		t.Fatalf("expected 2 records for site 1, got %d", len(history[1]))
	}
	if len(history[2]) != 1 {
		t.Fatalf("expected 1 record for site 2, got %d", len(history[2]))
	}

	upCount := 0
	for _, r := range history[1] {
		if r.IsUp {
			upCount++
		}
	}
	if upCount != 1 {
		t.Errorf("expected 1 up record for site 1, got %d", upCount)
	}
}

func TestDeleteSiteCascade(t *testing.T) {
	s := newTestStore(t)

	site := models.SiteConfig{Name: "Cascade Test", URL: "https://example.com", Interval: 30}
	if err := s.AddSite(context.Background(), site); err != nil {
		t.Fatalf("AddSite: %v", err)
	}
	sites, _ := s.GetSites(context.Background())
	siteID := sites[0].ID

	if err := s.SaveCheck(context.Background(), siteID, 1000, true); err != nil {
		t.Fatalf("SaveCheck: %v", err)
	}
	if err := s.SaveStateChange(context.Background(), siteID, "UP", "DOWN", "timeout"); err != nil {
		t.Fatalf("SaveStateChange: %v", err)
	}
	mw := models.MaintenanceWindow{
		MonitorID: siteID,
		Title:     "Test MW",
		Type:      "maintenance",
		StartTime: time.Now(),
	}
	if err := s.AddMaintenanceWindow(context.Background(), mw); err != nil {
		t.Fatalf("AddMaintenanceWindow: %v", err)
	}

	if err := s.DeleteSite(context.Background(), siteID); err != nil {
		t.Fatalf("DeleteSite: %v", err)
	}

	history, _ := s.LoadAllHistory(context.Background(), 100)
	if len(history[siteID]) != 0 {
		t.Errorf("expected 0 check_history rows, got %d", len(history[siteID]))
	}

	changes, _ := s.GetStateChanges(context.Background(), siteID, 100)
	if len(changes) != 0 {
		t.Errorf("expected 0 state_changes rows, got %d", len(changes))
	}

	windows, _ := s.GetActiveMaintenanceWindows(context.Background())
	for _, w := range windows {
		if w.MonitorID == siteID {
			t.Errorf("orphaned maintenance window found: id=%d", w.ID)
		}
	}
}

func TestPruneLogs(t *testing.T) {
	s := newTestStore(t)

	for i := 0; i < maxLogRows+50; i++ {
		if err := s.SaveLog(context.Background(), fmt.Sprintf("log %d", i)); err != nil {
			t.Fatalf("SaveLog: %v", err)
		}
	}
	if err := s.PruneLogs(context.Background()); err != nil {
		t.Fatalf("PruneLogs: %v", err)
	}

	logs, err := s.LoadLogs(context.Background(), maxLogRows*2)
	if err != nil {
		t.Fatalf("LoadLogs: %v", err)
	}
	if len(logs) != maxLogRows {
		t.Errorf("expected %d logs after prune, got %d", maxLogRows, len(logs))
	}
	// Newest must survive; oldest must be gone (membership, not position —
	// LoadLogs ordering ties when rows share a created_at second).
	present := make(map[string]bool, len(logs))
	for _, l := range logs {
		present[l.Message] = true
	}
	if !present[fmt.Sprintf("log %d", maxLogRows+50-1)] {
		t.Error("newest log was pruned")
	}
	if present["log 0"] {
		t.Error("oldest log survived prune")
	}
}

func TestPruneCheckHistory(t *testing.T) {
	s := newTestStore(t)

	for i := 0; i < maxCheckHistory+5; i++ {
		if err := s.SaveCheck(context.Background(), 1, int64(i), true); err != nil {
			t.Fatalf("SaveCheck site 1: %v", err)
		}
	}
	for i := 0; i < 3; i++ {
		if err := s.SaveCheck(context.Background(), 2, int64(i), true); err != nil {
			t.Fatalf("SaveCheck site 2: %v", err)
		}
	}

	if err := s.PruneCheckHistory(context.Background()); err != nil {
		t.Fatalf("PruneCheckHistory: %v", err)
	}

	history, err := s.LoadAllHistory(context.Background(), maxCheckHistory*2)
	if err != nil {
		t.Fatalf("LoadAllHistory: %v", err)
	}
	if len(history[1]) != maxCheckHistory {
		t.Errorf("site 1: expected %d rows after prune, got %d", maxCheckHistory, len(history[1]))
	}
	if len(history[2]) != 3 {
		t.Errorf("site 2: expected 3 rows untouched, got %d", len(history[2]))
	}
}

func TestPruneExpiredMaintenanceWindows(t *testing.T) {
	s := newTestStore(t)

	now := time.Now()

	// Expired 10 days ago — should be pruned with 7d retention.
	old := models.MaintenanceWindow{
		MonitorID: 0,
		Title:     "Old Window",
		Type:      "maintenance",
		StartTime: now.Add(-11 * 24 * time.Hour),
		EndTime:   now.Add(-10 * 24 * time.Hour),
	}
	if err := s.AddMaintenanceWindow(context.Background(), old); err != nil {
		t.Fatalf("AddMaintenanceWindow (old): %v", err)
	}

	// Expired 1 day ago — within 7d retention, should survive.
	recent := models.MaintenanceWindow{
		MonitorID: 0,
		Title:     "Recent Window",
		Type:      "maintenance",
		StartTime: now.Add(-2 * 24 * time.Hour),
		EndTime:   now.Add(-1 * 24 * time.Hour),
	}
	if err := s.AddMaintenanceWindow(context.Background(), recent); err != nil {
		t.Fatalf("AddMaintenanceWindow (recent): %v", err)
	}

	// Ongoing — no end time, should survive.
	ongoing := models.MaintenanceWindow{
		MonitorID: 0,
		Title:     "Ongoing Window",
		Type:      "maintenance",
		StartTime: now.Add(-1 * time.Hour),
	}
	if err := s.AddMaintenanceWindow(context.Background(), ongoing); err != nil {
		t.Fatalf("AddMaintenanceWindow (ongoing): %v", err)
	}

	pruned, err := s.PruneExpiredMaintenanceWindows(context.Background(), 7*24*time.Hour)
	if err != nil {
		t.Fatalf("PruneExpiredMaintenanceWindows: %v", err)
	}
	if pruned != 1 {
		t.Errorf("expected 1 pruned, got %d", pruned)
	}

	all, err := s.GetAllMaintenanceWindows(context.Background(), 100)
	if err != nil {
		t.Fatalf("GetAllMaintenanceWindows: %v", err)
	}
	if len(all) != 2 {
		t.Fatalf("expected 2 remaining windows, got %d", len(all))
	}
	for _, w := range all {
		if w.Title == "Old Window" {
			t.Error("old window should have been pruned")
		}
	}
}

func TestGetOverlappingMaintenanceWindows(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()

	site := models.SiteConfig{Name: "web", URL: "https://example.com", Interval: 30}
	if err := s.AddSite(ctx, site); err != nil {
		t.Fatalf("AddSite: %v", err)
	}

	now := time.Now()

	active := models.MaintenanceWindow{
		MonitorID: 1,
		Title:     "Deploy v2",
		Type:      "maintenance",
		StartTime: now.Add(-30 * time.Minute),
		EndTime:   now.Add(30 * time.Minute),
	}
	if err := s.AddMaintenanceWindow(ctx, active); err != nil {
		t.Fatalf("AddMaintenanceWindow: %v", err)
	}

	ended := models.MaintenanceWindow{
		MonitorID: 1,
		Title:     "Old deploy",
		Type:      "maintenance",
		StartTime: now.Add(-3 * time.Hour),
		EndTime:   now.Add(-2 * time.Hour),
	}
	if err := s.AddMaintenanceWindow(ctx, ended); err != nil {
		t.Fatalf("AddMaintenanceWindow: %v", err)
	}

	t.Run("same monitor overlaps", func(t *testing.T) {
		overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, now.Add(1*time.Hour))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(overlaps) != 1 {
			t.Fatalf("expected 1 overlap, got %d", len(overlaps))
		}
		if overlaps[0].Title != "Deploy v2" {
			t.Errorf("expected 'Deploy v2', got %q", overlaps[0].Title)
		}
	})

	t.Run("different monitor no overlap", func(t *testing.T) {
		overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 99, now, now.Add(1*time.Hour))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(overlaps) != 0 {
			t.Errorf("expected 0 overlaps, got %d", len(overlaps))
		}
	})

	t.Run("global window overlaps all", func(t *testing.T) {
		global := models.MaintenanceWindow{
			MonitorID: 0,
			Title:     "Global freeze",
			Type:      "maintenance",
			StartTime: now.Add(-10 * time.Minute),
			EndTime:   now.Add(2 * time.Hour),
		}
		if err := s.AddMaintenanceWindow(ctx, global); err != nil {
			t.Fatalf("AddMaintenanceWindow: %v", err)
		}
		overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, now.Add(1*time.Hour))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(overlaps) != 2 {
			t.Errorf("expected 2 overlaps (specific + global), got %d", len(overlaps))
		}
	})

	t.Run("indefinite window overlaps", func(t *testing.T) {
		overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, time.Time{})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(overlaps) < 1 {
			t.Error("expected at least 1 overlap for indefinite window")
		}
	})

	t.Run("ended window excluded", func(t *testing.T) {
		overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now.Add(-4*time.Hour), now.Add(-3*time.Hour))
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(overlaps) != 0 {
			t.Errorf("expected 0 overlaps for past range, got %d", len(overlaps))
		}
	})
}

// ImportData must encrypt alert settings (like AddAlert/UpdateAlert) so a
// restore with UPTOP_ENCRYPTION_KEY set never lands secrets in plaintext.
func TestImportData_EncryptsAlertSettings(t *testing.T) {
	s := newTestStore(t)
	enc, err := NewEncryptor(strings.Repeat("ab", 32)) // 64 hex chars = 32 bytes
	if err != nil {
		t.Fatalf("NewEncryptor: %v", err)
	}
	s.SetEncryptor(enc)

	backup := models.Backup{
		Alerts: []models.AlertConfig{
			{ID: 1, Name: "tg", Type: "telegram", Settings: map[string]string{"token": "123:SECRET", "chat_id": "42"}},
		},
	}
	if err := s.ImportData(context.Background(), backup); err != nil {
		t.Fatalf("ImportData: %v", err)
	}

	var raw string
	if err := s.db.QueryRow("SELECT settings FROM alerts WHERE id = 1").Scan(&raw); err != nil {
		t.Fatalf("query settings: %v", err)
	}
	if !strings.HasPrefix(raw, encryptedPrefix) {
		t.Errorf("imported settings not encrypted: %q", raw)
	}
	if strings.Contains(raw, "SECRET") {
		t.Errorf("plaintext secret found in stored column: %q", raw)
	}

	alerts, err := s.GetAllAlerts(context.Background())
	if err != nil {
		t.Fatalf("GetAllAlerts: %v", err)
	}
	if len(alerts) != 1 || alerts[0].Settings["token"] != "123:SECRET" {
		t.Errorf("decrypt round-trip failed: %+v", alerts)
	}
}
