package handlers

import (
	"archive/zip"
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"

	authpkg "armature/auth"
	"armature/config"
	"armature/database"
	"armature/middleware"
	"armature/store"
	"armature/store/sqlite"
)

// ── Backup Test Harness ────────────────────────

type backupHarness struct {
	*testHarness
	adminToken string
	adminID    string
	stores     store.Stores
	backupH    *BackupHandler
}

func setupBackupHarness(t *testing.T) *backupHarness {
	t.Helper()
	database.RequireTestDB(t)
	database.TruncateAll(t)

	cfg := &config.Config{
		JWTSecret: testJWTSecret,
		BasePath:  "",
	}

	var stores store.Stores
	if database.IsSQLite() {
		stores = sqlite.NewStores(database.TestDB)
	} else {
		t.Skip("backup tests run on SQLite only")
	}
	userCache := middleware.NewUserStatusCache()
	storagePath := t.TempDir()
	backupH := NewBackupHandler(stores, "", storagePath)

	r := gin.New()
	api := r.Group("/api/v1")

	// Auth
	auth := NewAuthHandler(cfg, stores, nil, authpkg.NewBuiltinProvider())
	api.POST("/auth/register", auth.Register)
	api.POST("/auth/login", auth.Login)

	// Admin group
	admin := api.Group("/admin")
	admin.Use(middleware.Auth(cfg, stores.Users, userCache), middleware.RequireAdmin(stores))
	admin.POST("/backup", backupH.CreateBackup)
	admin.GET("/backups", backupH.ListBackups)
	admin.GET("/backups/:name", backupH.DownloadBackup)
	admin.DELETE("/backups/:name", backupH.DeleteBackup)
	admin.POST("/restore", backupH.RestoreBackup)

	// Seed admin
	adminID := seedInsertReturningID(t,
		`INSERT INTO users (username, email, password_hash, handle, auth_source) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
		"bk-admin", "bk-admin@test.com", "$2a$10$test", "bk-admin", "builtin",
	)
	database.SeedEveryoneGroupMember(t, adminID)
	database.SeedAdminsGroupMember(t, adminID)
	adminToken := makeToken(adminID, "bk-admin@test.com", "admin")

	return &backupHarness{
		testHarness: &testHarness{router: r, t: t},
		adminToken:  adminToken,
		adminID:     adminID,
		stores:      stores,
		backupH:     backupH,
	}
}

// ── Tests ──────────────────────────────────────

func TestCreateBackup_Basic(t *testing.T) {
	h := setupBackupHarness(t)

	w := h.request("POST", "/api/v1/admin/backup", h.adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify it's a valid ZIP
	data := w.Body.Bytes()
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		t.Fatalf("invalid ZIP: %v", err)
	}

	// Must contain meta.json
	var foundMeta bool
	for _, f := range zr.File {
		if f.Name == "meta.json" {
			foundMeta = true
			rc, _ := f.Open()
			var meta backupMeta
			json.NewDecoder(rc).Decode(&meta)
			rc.Close()
			if meta.Dialect != "sqlite" {
				t.Errorf("expected dialect sqlite, got %s", meta.Dialect)
			}
			if len(meta.CoreTables) == 0 {
				t.Error("expected core tables in meta")
			}
		}
	}
	if !foundMeta {
		t.Fatal("meta.json not found in backup")
	}

	// Must contain core table JSONL files
	var foundUsers bool
	for _, f := range zr.File {
		if f.Name == "core/users.jsonl" {
			foundUsers = true
		}
	}
	if !foundUsers {
		t.Error("core/users.jsonl not found in backup")
	}
}

func TestCreateBackup_WithExtData(t *testing.T) {
	h := setupBackupHarness(t)
	ctx := context.Background()

	// Seed a package so FK constraint is satisfied
	database.TestDB.ExecContext(ctx, dialectSQL(
		`INSERT INTO packages (id, title, type, tier, version, source, enabled, manifest)
		 VALUES ($1, $2, $3, $4, $5, $6, 1, '{}')`),
		"testpkg", "Test Pkg", "extension", "starlark", "0.1.0", "extension",
	)

	// Register an ext_data table and create it
	database.TestDB.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS ext_testpkg_items (
		id TEXT PRIMARY KEY,
		name TEXT NOT NULL,
		created_at TEXT DEFAULT (datetime('now'))
	)`)
	if _, err := database.TestDB.ExecContext(ctx,
		dialectSQL(`INSERT INTO ext_data_tables (package_id, table_name) VALUES ($1, $2)`),
		"testpkg", "items",
	); err != nil {
		t.Fatalf("insert ext_data_tables: %v", err)
	}
	database.TestDB.ExecContext(ctx,
		`INSERT INTO ext_testpkg_items (id, name) VALUES ('item1', 'Test Item')`,
	)
	defer database.TestDB.ExecContext(ctx, "DROP TABLE IF EXISTS ext_testpkg_items")

	w := h.request("POST", "/api/v1/admin/backup", h.adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}

	data := w.Body.Bytes()
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		t.Fatalf("invalid ZIP: %v", err)
	}

	// Should contain ext_data/testpkg/items.jsonl
	var foundExt bool
	for _, f := range zr.File {
		if f.Name == "ext_data/testpkg/items.jsonl" {
			foundExt = true
			rc, _ := f.Open()
			scanner := bufio.NewScanner(rc)
			if !scanner.Scan() {
				t.Error("ext_data file is empty")
			} else {
				var row map[string]interface{}
				json.Unmarshal(scanner.Bytes(), &row)
				if row["name"] != "Test Item" {
					t.Errorf("expected 'Test Item', got %v", row["name"])
				}
			}
			rc.Close()
		}
	}
	if !foundExt {
		t.Error("ext_data/testpkg/items.jsonl not found")
	}

	// Check meta.json has ext table listed
	metaFile := findInZip(zr, "meta.json")
	rc, _ := metaFile.Open()
	var meta backupMeta
	json.NewDecoder(rc).Decode(&meta)
	rc.Close()
	found := false
	for _, et := range meta.ExtTables {
		if et == "ext_testpkg_items" {
			found = true
		}
	}
	if !found {
		t.Errorf("ext_testpkg_items not in meta.ExtTables: %v", meta.ExtTables)
	}
}

func TestRestoreBackup_RoundTrip(t *testing.T) {
	h := setupBackupHarness(t)

	// Create a second user so we have data to verify
	seedInsertReturningID(t,
		`INSERT INTO users (username, email, password_hash, handle, auth_source) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
		"bk-user2", "bk-user2@test.com", "$2a$10$test", "bk-user2", "builtin",
	)

	// Create backup
	w := h.request("POST", "/api/v1/admin/backup", h.adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("backup: expected 200, got %d: %s", w.Code, w.Body.String())
	}
	backupData := w.Body.Bytes()

	// Verify we have 2 users
	var countBefore int
	database.TestDB.QueryRow("SELECT COUNT(*) FROM users").Scan(&countBefore)
	if countBefore < 2 {
		t.Fatalf("expected at least 2 users, got %d", countBefore)
	}

	// Restore directly (the handler wipes + restores, so data should round-trip)
	// We use a separate router without admin middleware for restore,
	// since the wipe inside RestoreBackup removes the admin group membership
	// mid-request. In production, restore would use a session-based token
	// that survives the wipe. For testing, we bypass the middleware.
	restoreRouter := gin.New()
	restoreRouter.POST("/restore", h.backupH.RestoreBackup)

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, _ := writer.CreateFormFile("file", "test-backup.swb")
	part.Write(backupData)
	writer.Close()

	req := httptest.NewRequest("POST", "/restore", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	rw := httptest.NewRecorder()
	restoreRouter.ServeHTTP(rw, req)

	if rw.Code != http.StatusOK {
		t.Fatalf("restore: expected 200, got %d: %s", rw.Code, rw.Body.String())
	}

	// Verify data is back
	var countAfterRestore int
	database.TestDB.QueryRow("SELECT COUNT(*) FROM users").Scan(&countAfterRestore)
	if countAfterRestore != countBefore {
		t.Errorf("expected %d users after restore, got %d", countBefore, countAfterRestore)
	}

	// Verify specific user exists
	var username string
	err := database.TestDB.QueryRow("SELECT username FROM users WHERE username = 'bk-user2'").Scan(&username)
	if err != nil {
		t.Errorf("user bk-user2 not found after restore: %v", err)
	}
}

func TestRestoreBackup_SchemaVersionMismatch(t *testing.T) {
	h := setupBackupHarness(t)

	// Build a fake backup with a future schema version
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	mw, _ := zw.Create("meta.json")
	json.NewEncoder(mw).Encode(backupMeta{
		Version:       "99.0.0",
		SchemaVersion: "999_future",
		Dialect:       "sqlite",
		CoreTables:    []string{"users"},
	})
	zw.Close()

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, _ := writer.CreateFormFile("file", "future.swb")
	part.Write(buf.Bytes())
	writer.Close()

	req := httptest.NewRequest("POST", "/api/v1/admin/restore", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	req.Header.Set("Authorization", "Bearer "+h.adminToken)
	rw := httptest.NewRecorder()
	h.router.ServeHTTP(rw, req)

	if rw.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for future schema, got %d: %s", rw.Code, rw.Body.String())
	}
}

func TestDumpAndRestoreTable(t *testing.T) {
	database.RequireTestDB(t)
	ctx := context.Background()
	db := database.TestDB

	// Create a test table
	db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS test_backup_round (
		id TEXT PRIMARY KEY,
		name TEXT NOT NULL,
		count INTEGER DEFAULT 0
	)`)
	defer db.ExecContext(ctx, "DROP TABLE IF EXISTS test_backup_round")

	// Insert data
	db.ExecContext(ctx, `INSERT INTO test_backup_round (id, name, count) VALUES ('r1', 'alpha', 10)`)
	db.ExecContext(ctx, `INSERT INTO test_backup_round (id, name, count) VALUES ('r2', 'beta', 20)`)

	// Dump
	var buf bytes.Buffer
	count, err := dumpTable(ctx, db, "test_backup_round", &buf)
	if err != nil {
		t.Fatalf("dump: %v", err)
	}
	if count != 2 {
		t.Fatalf("dump: expected 2 rows, got %d", count)
	}

	// Wipe
	db.ExecContext(ctx, "DELETE FROM test_backup_round")

	// Restore
	restored, err := restoreTable(ctx, db, "test_backup_round", &buf)
	if err != nil {
		t.Fatalf("restore: %v", err)
	}
	if restored != 2 {
		t.Fatalf("restore: expected 2 rows, got %d", restored)
	}

	// Verify
	var name string
	var cnt int
	db.QueryRowContext(ctx, "SELECT name, count FROM test_backup_round WHERE id = 'r1'").Scan(&name, &cnt)
	if name != "alpha" || cnt != 10 {
		t.Errorf("expected alpha/10, got %s/%d", name, cnt)
	}
}

func TestListBackups_Empty(t *testing.T) {
	h := setupBackupHarness(t)

	w := h.request("GET", "/api/v1/admin/backups", h.adminToken, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}

	var resp struct{ Data []interface{} }
	json.NewDecoder(w.Body).Decode(&resp)
	if len(resp.Data) != 0 {
		t.Errorf("expected empty list, got %d", len(resp.Data))
	}
}
