package appstate

import (
	"archive/zip"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

func TestEnsureRotatingLocalBackupCreatesAndRotates(t *testing.T) {
	temp := t.TempDir()
	dbPath := filepath.Join(temp, "qfs.db")
	cfgPath := filepath.Join(temp, "config.yaml")

	if err := writeTestSQLiteDB(dbPath); err != nil {
		t.Fatalf("write sqlite db: %v", err)
	}
	if err := os.WriteFile(cfgPath, []byte("cfg"), 0644); err != nil {
		t.Fatalf("write config: %v", err)
	}

	prevNow := backupNow
	defer func() { backupNow = prevNow }()
	backupNow = func() time.Time { return time.Date(2026, 2, 11, 10, 0, 0, 0, time.UTC) }

	created, err := EnsureRotatingLocalBackup(dbPath, cfgPath)
	if err != nil {
		t.Fatalf("backup: %v", err)
	}
	if len(created) == 0 {
		t.Fatalf("expected backup to be created")
	}

	dailyArchive := filepath.Join(temp, "backups", "daily", "qfs-backp-2026-02-11.zip")
	if _, err := os.Stat(dailyArchive); err != nil {
		t.Fatalf("daily archive missing: %v", err)
	}
	assertZipContains(t, dailyArchive, "qfs.db", "config.yaml")

	backupNow = func() time.Time { return time.Date(2026, 2, 12, 10, 0, 0, 0, time.UTC) }
	created, err = EnsureRotatingLocalBackup(dbPath, cfgPath)
	if err != nil {
		t.Fatalf("backup rotate: %v", err)
	}
	if len(created) == 0 {
		t.Fatalf("expected backup to be created for new day")
	}

	dailyArchive = filepath.Join(temp, "backups", "daily", "qfs-backp-2026-02-12.zip")
	if _, err := os.Stat(dailyArchive); err != nil {
		t.Fatalf("daily archive missing after rotate: %v", err)
	}
}

func TestEnsureRotatingLocalBackupEnvControls(t *testing.T) {
	temp := t.TempDir()
	dbPath := filepath.Join(temp, "qfs.db")
	cfgPath := filepath.Join(temp, "config.yaml")

	if err := writeTestSQLiteDB(dbPath); err != nil {
		t.Fatalf("write sqlite db: %v", err)
	}
	if err := os.WriteFile(cfgPath, []byte("cfg"), 0644); err != nil {
		t.Fatalf("write config: %v", err)
	}

	backupRoot := filepath.Join(temp, "custom_backups")
	t.Setenv(envBackupDir, backupRoot)

	if _, err := EnsureRotatingLocalBackup(dbPath, cfgPath); err != nil {
		t.Fatalf("backup with env: %v", err)
	}
	if _, err := os.Stat(filepath.Join(backupRoot, "daily", ".period.json")); err != nil {
		t.Fatalf("expected backup in custom dir: %v", err)
	}

	t.Setenv(envBackupDisable, "1")
	if _, err := EnsureRotatingLocalBackup(dbPath, cfgPath); err != nil {
		t.Fatalf("backup disabled: %v", err)
	}
	if _, err := os.Stat(filepath.Join(backupRoot, "daily", ".period.json")); err != nil {
		t.Fatalf("backup should remain from previous run: %v", err)
	}
}

func TestEnsureRotatingLocalBackupRejectsGitWorktree(t *testing.T) {
	temp := t.TempDir()
	repoRoot := filepath.Join(temp, "repo")
	if err := os.MkdirAll(filepath.Join(repoRoot, ".git"), 0755); err != nil {
		t.Fatalf("mkdir git dir: %v", err)
	}

	dbPath := filepath.Join(repoRoot, "data", "qfs.db")
	cfgPath := filepath.Join(repoRoot, "data", "config.yaml")
	if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
		t.Fatalf("mkdir data dir: %v", err)
	}
	if err := writeTestSQLiteDB(dbPath); err != nil {
		t.Fatalf("write sqlite db: %v", err)
	}
	if err := os.WriteFile(cfgPath, []byte("cfg"), 0644); err != nil {
		t.Fatalf("write cfg: %v", err)
	}

	_, err := EnsureRotatingLocalBackup(dbPath, cfgPath)
	if err == nil {
		t.Fatal("expected git worktree backup root to be rejected")
	}
	if !strings.Contains(err.Error(), "outside git worktree") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func writeTestSQLiteDB(path string) error {
	db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
	if err != nil {
		return err
	}
	sqlDB, err := db.DB()
	if err != nil {
		return err
	}
	defer sqlDB.Close()

	return db.Exec(`
		CREATE TABLE sample_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL
		);
		INSERT INTO sample_items(name) VALUES ('backup');
	`).Error
}

func assertZipContains(t *testing.T, archivePath string, expected ...string) {
	t.Helper()

	reader, err := zip.OpenReader(archivePath)
	if err != nil {
		t.Fatalf("open archive: %v", err)
	}
	defer reader.Close()

	found := make(map[string]bool, len(reader.File))
	for _, file := range reader.File {
		found[file.Name] = true
	}
	for _, name := range expected {
		if !found[name] {
			t.Fatalf("archive %s missing %s", archivePath, name)
		}
	}
}
