package handlers

import (
	"archive/zip"
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"

	"switchboard-core/database"
	"switchboard-core/store"
)

// ── Helpers ────────────────────────────────────

// buildPkgBytes creates a .pkg archive in memory and returns the ZIP bytes.
func buildPkgBytes(t *testing.T, manifest map[string]any) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	data, _ := json.Marshal(manifest)
	w, _ := zw.Create("manifest.json")
	w.Write(data)
	zw.Close()
	return buf.Bytes()
}

// seedPackage installs a package directly into the DB for testing.
func seedPackage(t *testing.T, stores store.Stores, manifest map[string]any) {
	t.Helper()
	ctx := context.Background()
	id := manifest["id"].(string)
	title := manifest["title"].(string)
	source := "extension"
	if s, ok := manifest["source"].(string); ok {
		source = s
	}
	if err := stores.Packages.Seed(ctx, id, title, source, manifest); err != nil {
		t.Fatal(err)
	}
	version, _ := manifest["version"].(string)
	if version == "" {
		version = "0.0.0"
	}
	pkgType, _ := manifest["type"].(string)
	if pkgType == "" {
		pkgType = "surface"
	}
	tier, _ := manifest["tier"].(string)
	if tier == "" {
		tier = "browser"
	}
	pkg := &store.PackageRegistration{
		Title:    title,
		Type:     pkgType,
		Version:  version,
		Tier:     tier,
		Enabled:  true,
		Manifest: manifest,
	}
	if err := stores.Packages.Update(ctx, id, pkg); err != nil {
		t.Fatalf("seedPackage Update failed for %s: %v", id, err)
	}
	// Verify version was actually stored
	check, _ := stores.Packages.Get(ctx, id)
	if check != nil && check.Version != version {
		t.Fatalf("seedPackage: version not stored correctly for %s: got %q want %q", id, check.Version, version)
	}
}

// setupUpdateRouter creates a gin router with the update and export routes.
func setupUpdateRouter(t *testing.T, stores store.Stores) *gin.Engine {
	t.Helper()
	gin.SetMode(gin.TestMode)
	r := gin.New()
	handler := NewPackageHandler(stores)
	r.POST("/api/v1/admin/packages/:id/update", handler.UpdatePackage)
	r.GET("/api/v1/admin/packages/:id/export", handler.ExportPackage)
	return r
}

// doUpdate performs a multipart upload to the update endpoint.
func doUpdate(router *gin.Engine, pkgID string, pkgBytes []byte) *httptest.ResponseRecorder {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, _ := writer.CreateFormFile("file", "test.pkg")
	part.Write(pkgBytes)
	writer.Close()

	req := httptest.NewRequest("POST", "/api/v1/admin/packages/"+pkgID+"/update", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

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

func TestUpdatePackage_VersionBump(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	seedPackage(t, stores, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.1.0",
	})

	w := doUpdate(router, "test-pkg", pkg)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify version updated
	updated, _ := stores.Packages.Get(ctx, "test-pkg")
	if updated.Version != "1.1.0" {
		t.Errorf("version = %q, want %q", updated.Version, "1.1.0")
	}

	// Check response body
	var resp map[string]any
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["version"] != "1.1.0" {
		t.Errorf("response version = %v, want 1.1.0", resp["version"])
	}
	if resp["previous_version"] != "1.0.0" {
		t.Errorf("response previous_version = %v, want 1.0.0", resp["previous_version"])
	}
}

func TestUpdatePackage_SameVersion_Rejected(t *testing.T) {
	stores := newTestStores(t)

	seedPackage(t, stores, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	w := doUpdate(router, "test-pkg", pkg)
	if w.Code != http.StatusConflict {
		t.Fatalf("expected 409, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_OlderVersion_Rejected(t *testing.T) {
	stores := newTestStores(t)

	seedPackage(t, stores, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "2.0.0",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	w := doUpdate(router, "test-pkg", pkg)
	if w.Code != http.StatusConflict {
		t.Fatalf("expected 409, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_CorePackage_Rejected(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	// Seed as core package
	stores.Packages.Seed(ctx, "core-pkg", "Core", "core", map[string]any{
		"id": "core-pkg", "title": "Core", "type": "surface",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "core-pkg", "title": "Core", "type": "surface", "version": "2.0.0",
	})

	w := doUpdate(router, "core-pkg", pkg)
	if w.Code != http.StatusForbidden {
		t.Fatalf("expected 403, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_TypeMismatch_Rejected(t *testing.T) {
	stores := newTestStores(t)

	seedPackage(t, stores, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "extension", "version": "2.0.0",
		"tools": []any{map[string]any{"name": "test"}},
	})

	w := doUpdate(router, "test-pkg", pkg)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_IDMismatch_Rejected(t *testing.T) {
	stores := newTestStores(t)

	seedPackage(t, stores, map[string]any{
		"id": "test-pkg", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "other-pkg", "title": "Other", "type": "surface", "version": "2.0.0",
	})

	w := doUpdate(router, "test-pkg", pkg)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_NotFound(t *testing.T) {
	stores := newTestStores(t)

	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "nonexistent", "title": "Test", "type": "surface", "version": "1.0.0",
	})

	w := doUpdate(router, "nonexistent", pkg)
	if w.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String())
	}
}

func TestUpdatePackage_SchemaAddColumn(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	// Install v1 with a table that has one column
	seedPackage(t, stores, map[string]any{
		"id": "schema-pkg", "title": "Schema Test", "type": "surface", "version": "1.0.0",
		"db_tables": map[string]any{
			"items": map[string]any{
				"columns": map[string]any{"name": "text"},
			},
		},
	})
	// Actually create the table
	tables, _ := ParseDBTables(map[string]any{
		"db_tables": map[string]any{
			"items": map[string]any{
				"columns": map[string]any{"name": "text"},
			},
		},
	})
	CreateExtTables(ctx, database.TestDB, database.IsPostgres(), stores, "schema-pkg", tables)

	// Insert a row to verify data preservation
	physical := extPhysicalTable("schema-pkg", "items")
	_, err := database.TestDB.ExecContext(ctx,
		fmt.Sprintf("INSERT INTO %s (id, name) VALUES ('row1', 'hello')", physical))
	if err != nil {
		t.Fatal(err)
	}

	// Update to v2 with an added column
	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "schema-pkg", "title": "Schema Test", "type": "surface", "version": "2.0.0",
		"db_tables": map[string]any{
			"items": map[string]any{
				"columns": map[string]any{"name": "text", "priority": "int"},
			},
		},
	})

	w := doUpdate(router, "schema-pkg", pkg)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify new column exists and old data preserved
	var name string
	var priority sql.NullInt64
	err = database.TestDB.QueryRowContext(ctx,
		fmt.Sprintf("SELECT name, priority FROM %s WHERE id = 'row1'", physical)).
		Scan(&name, &priority)
	if err != nil {
		t.Fatal("query failed after schema migration:", err)
	}
	if name != "hello" {
		t.Errorf("name = %q, want %q (data should be preserved)", name, "hello")
	}
}

func TestUpdatePackage_SchemaAddTable(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	// Install v1 with no tables
	seedPackage(t, stores, map[string]any{
		"id": "schema-pkg-b", "title": "Schema B", "type": "surface", "version": "1.0.0",
	})

	// Update to v2 with a new table
	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "schema-pkg-b", "title": "Schema B", "type": "surface", "version": "2.0.0",
		"db_tables": map[string]any{
			"audit": map[string]any{
				"columns": map[string]any{"action": "text", "actor": "text"},
			},
		},
	})

	w := doUpdate(router, "schema-pkg-b", pkg)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify table was created
	physical := extPhysicalTable("schema-pkg-b", "audit")
	_, err := database.TestDB.ExecContext(ctx,
		fmt.Sprintf("INSERT INTO %s (id, action, actor) VALUES ('row1', 'test', 'user1')", physical))
	if err != nil {
		t.Fatal("new table should exist after update:", err)
	}
}

func TestUpdatePackage_SettingsMerge(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	// Install with initial settings
	seedPackage(t, stores, map[string]any{
		"id": "settings-pkg", "title": "Settings Test", "type": "surface", "version": "1.0.0",
		"settings": []any{
			map[string]any{"key": "color", "type": "text", "default": "blue"},
		},
	})
	// Set existing settings value
	existing := json.RawMessage(`{"color":"red"}`)
	stores.Packages.SetPackageSettings(ctx, "settings-pkg", existing)

	// Update with new settings key
	router := setupUpdateRouter(t, stores)
	pkg := buildPkgBytes(t, map[string]any{
		"id": "settings-pkg", "title": "Settings Test", "type": "surface", "version": "2.0.0",
		"settings": []any{
			map[string]any{"key": "color", "type": "text", "default": "blue"},
			map[string]any{"key": "size", "type": "int", "default": float64(12)},
		},
	})

	w := doUpdate(router, "settings-pkg", pkg)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify settings: color preserved as "red", size added with default 12
	raw, _ := stores.Packages.GetPackageSettings(ctx, "settings-pkg")
	var settings map[string]any
	json.Unmarshal(raw, &settings)

	if settings["color"] != "red" {
		t.Errorf("color = %v, want %q (existing should be preserved)", settings["color"], "red")
	}
	if settings["size"] != float64(12) {
		t.Errorf("size = %v, want %v (new key should get default)", settings["size"], 12)
	}
}

func TestExportPackage_Basic(t *testing.T) {
	stores := newTestStores(t)

	seedPackage(t, stores, map[string]any{
		"id": "export-pkg", "title": "Export Test", "type": "surface", "version": "1.0.0",
	})

	router := setupUpdateRouter(t, stores)

	req := httptest.NewRequest("GET", "/api/v1/admin/packages/export-pkg/export", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	// Verify response is a valid zip with manifest.json
	zr, err := zip.NewReader(bytes.NewReader(w.Body.Bytes()), int64(w.Body.Len()))
	if err != nil {
		t.Fatal("response is not a valid zip:", err)
	}

	foundManifest := false
	for _, f := range zr.File {
		if f.Name == "manifest.json" {
			foundManifest = true
			rc, _ := f.Open()
			data, _ := io.ReadAll(rc)
			rc.Close()
			var m map[string]any
			if json.Unmarshal(data, &m) != nil {
				t.Fatal("manifest.json is not valid JSON")
			}
			if m["id"] != "export-pkg" {
				t.Errorf("manifest id = %v, want export-pkg", m["id"])
			}
		}
	}
	if !foundManifest {
		t.Error("zip does not contain manifest.json")
	}
}

func TestExportPackage_NotFound(t *testing.T) {
	stores := newTestStores(t)
	router := setupUpdateRouter(t, stores)

	req := httptest.NewRequest("GET", "/api/v1/admin/packages/nonexistent/export", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	if w.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", w.Code)
	}
}

// TestBundledInstall_DefaultAllowlist verifies the curated default set behavior.
func TestBundledInstall_DefaultAllowlist(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	bundledDir := t.TempDir()
	packagesDir := t.TempDir()

	// Create packages: one in default set, one not
	buildTestPkg(t, bundledDir, map[string]any{
		"id": "notes", "title": "Notes", "type": "surface", "version": "1.0.0",
	})
	buildTestPkg(t, bundledDir, map[string]any{
		"id": "regex-tester", "title": "Regex Tester", "type": "extension", "version": "1.0.0",
	})

	// Empty allowlist → curated defaults
	InstallBundledPackages(bundledDir, packagesDir, "", stores, nil)

	// "notes" is in the default set → should be installed
	pkg, err := stores.Packages.Get(ctx, "notes")
	if err != nil || pkg == nil {
		t.Fatal("notes should be installed (in default set)")
	}

	// "regex-tester" is NOT in the default set → should be filtered
	pkg, _ = stores.Packages.Get(ctx, "regex-tester")
	if pkg != nil {
		t.Fatal("regex-tester should NOT be installed (not in default set)")
	}
}

// TestBundledInstall_WildcardAllowlist verifies "*" installs everything.
func TestBundledInstall_WildcardAllowlist(t *testing.T) {
	stores := newTestStores(t)
	ctx := context.Background()

	bundledDir := t.TempDir()
	packagesDir := t.TempDir()

	buildTestPkg(t, bundledDir, map[string]any{
		"id": "any-pkg", "title": "Any", "type": "surface", "version": "1.0.0",
	})

	InstallBundledPackages(bundledDir, packagesDir, "*", stores, nil)

	pkg, err := stores.Packages.Get(ctx, "any-pkg")
	if err != nil || pkg == nil {
		t.Fatal("any-pkg should be installed with * wildcard")
	}
}
