package handlers

import (
	"context"
	"encoding/json"
	"net/http"
	"testing"

	"github.com/gin-gonic/gin"

	"chat-switchboard/config"
	"chat-switchboard/database"
	"chat-switchboard/middleware"
	"chat-switchboard/models"
	"chat-switchboard/store"
	postgres "chat-switchboard/store/postgres"
	sqlite "chat-switchboard/store/sqlite"
)

// ── Project Test Harness ──────────────────

type projectHarness struct {
	*testHarness
	stores     store.Stores
	userToken  string
	userID     string
	adminToken string
	adminID    string
}

func setupProjectHarness(t *testing.T) *projectHarness {
	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 {
		stores = postgres.NewStores(database.TestDB)
	}
	userCache := middleware.NewUserStatusCache()

	r := gin.New()
	api := r.Group("/api/v1")
	protected := api.Group("")
	protected.Use(middleware.Auth(cfg, stores.Users, userCache))

	projH := NewProjectHandler(stores)
	protected.GET("/projects", projH.List)
	protected.POST("/projects", projH.Create)
	protected.GET("/projects/:id", projH.Get)
	protected.PUT("/projects/:id", projH.Update)
	protected.DELETE("/projects/:id", projH.Delete)
	protected.POST("/projects/:id/channels", projH.AddChannel)
	protected.DELETE("/projects/:id/channels/:channelId", projH.RemoveChannel)
	protected.GET("/projects/:id/channels", projH.ListChannels)
	protected.PUT("/projects/:id/channels/reorder", projH.ReorderChannels)
	protected.POST("/projects/:id/knowledge-bases", projH.AddKB)
	protected.DELETE("/projects/:id/knowledge-bases/:kbId", projH.RemoveKB)
	protected.GET("/projects/:id/knowledge-bases", projH.ListKBs)
	protected.POST("/projects/:id/notes", projH.AddNote)
	protected.DELETE("/projects/:id/notes/:noteId", projH.RemoveNote)
	protected.GET("/projects/:id/notes", projH.ListNotes)

	// Admin routes
	admin := api.Group("/admin")
	admin.Use(middleware.Auth(cfg, stores.Users, userCache))
	admin.Use(middleware.RequireAdmin())
	admin.GET("/projects", projH.AdminList)
	admin.DELETE("/projects/:id", projH.Delete)

	userID := database.SeedTestUser(t, "projuser", "projuser@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
	userToken := makeToken(userID, "projuser@test.com", "user")

	adminID := database.SeedTestUser(t, "projadmin", "projadmin@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true, role = 'admin' WHERE id = $1"), adminID)
	adminToken := makeToken(adminID, "projadmin@test.com", "admin")

	return &projectHarness{
		testHarness: &testHarness{router: r, t: t},
		stores:      stores,
		userToken:   userToken,
		userID:      userID,
		adminToken:  adminToken,
		adminID:     adminID,
	}
}

// seedProject creates a project via the store and returns it.
func (h *projectHarness) seedProject(name, ownerID string) *models.Project {
	h.t.Helper()
	p := &models.Project{
		Name:    name,
		Scope:   models.ScopePersonal,
		OwnerID: ownerID,
	}
	if err := h.stores.Projects.Create(context.Background(), p); err != nil {
		h.t.Fatalf("seedProject: %v", err)
	}
	return p
}

// ── GET /projects — envelope ──────────────

func TestProjects_List_Empty_Envelope(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("GET", "/api/v1/projects", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	data, ok := body["data"]
	if !ok {
		t.Fatal("GET /projects must return {\"data\": [...]}, missing \"data\" key")
	}
	arr, ok := data.([]interface{})
	if !ok {
		t.Fatalf("data should be an array, got %T", data)
	}
	if len(arr) != 0 {
		t.Errorf("expected empty array, got %d items", len(arr))
	}
}

func TestProjects_List_WithData_Shape(t *testing.T) {
	h := setupProjectHarness(t)
	h.seedProject("test-proj", h.userID)

	resp := h.request("GET", "/api/v1/projects", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	data, ok := body["data"].([]interface{})
	if !ok {
		t.Fatal("data must be an array")
	}
	if len(data) != 1 {
		t.Fatalf("expected 1 project, got %d", len(data))
	}

	proj := data[0].(map[string]interface{})
	for _, key := range []string{"id", "name", "scope", "owner_id", "is_archived", "created_at", "updated_at"} {
		if _, exists := proj[key]; !exists {
			t.Errorf("missing field %q in project object", key)
		}
	}
	if proj["name"] != "test-proj" {
		t.Errorf("name: got %v, want test-proj", proj["name"])
	}
}

// ── POST /projects — create ──────────────

func TestProjects_Create_201_Shape(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("POST", "/api/v1/projects", h.userToken, map[string]interface{}{
		"name":        "new-project",
		"description": "test desc",
		"color":       "#ff0000",
		"icon":        "star",
	})
	if resp.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	// Bare object, not wrapped in "data"
	if _, exists := body["data"]; exists {
		t.Error("POST /projects should return bare object, not wrapped in 'data'")
	}
	if body["name"] != "new-project" {
		t.Errorf("name: got %v, want new-project", body["name"])
	}
	if body["scope"] != "personal" {
		t.Errorf("scope should be forced to personal, got %v", body["scope"])
	}
	if body["owner_id"] != h.userID {
		t.Errorf("owner_id: got %v, want %s", body["owner_id"], h.userID)
	}
	if body["color"] != "#ff0000" {
		t.Errorf("color: got %v, want #ff0000", body["color"])
	}
	if body["icon"] != "star" {
		t.Errorf("icon: got %v, want star", body["icon"])
	}
}

func TestProjects_Create_RequiresName(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("POST", "/api/v1/projects", h.userToken, map[string]interface{}{
		"description": "no name",
	})
	if resp.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for missing name, got %d", resp.Code)
	}
}

// ── GET /projects/:id — single object ─────

func TestProjects_Get_Shape(t *testing.T) {
	h := setupProjectHarness(t)
	p := h.seedProject("detail-proj", h.userID)

	resp := h.request("GET", "/api/v1/projects/"+p.ID, h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	// Bare object
	if _, exists := body["data"]; exists {
		t.Error("GET /:id should return object directly, not wrapped in 'data'")
	}
	if body["id"] != p.ID {
		t.Errorf("id: got %v, want %s", body["id"], p.ID)
	}
	// Computed counts use omitempty — absent when zero, present as number when nonzero.
	// With a fresh project all counts are 0, so they'll be omitted. Verify that if
	// present they're numeric (the AdminList test covers nonzero count visibility).
	for _, key := range []string{"channel_count", "kb_count", "note_count"} {
		if val, exists := body[key]; exists {
			if _, ok := val.(float64); !ok {
				t.Errorf("%s should be numeric, got %T", key, val)
			}
		}
	}
}

func TestProjects_Get_NotFound(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("GET", "/api/v1/projects/00000000-0000-0000-0000-000000000000", h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", resp.Code)
	}
}

// ── PUT /projects/:id — update ────────────

func TestProjects_Update_ReturnsRefreshed(t *testing.T) {
	h := setupProjectHarness(t)
	p := h.seedProject("old-name", h.userID)

	resp := h.request("PUT", "/api/v1/projects/"+p.ID, h.userToken, map[string]interface{}{
		"name": "new-name",
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if body["name"] != "new-name" {
		t.Errorf("name should be updated to new-name, got %v", body["name"])
	}
	if body["id"] != p.ID {
		t.Errorf("id mismatch: got %v, want %s", body["id"], p.ID)
	}
}

// ── DELETE /projects/:id ──────────────────

func TestProjects_Delete_Shape(t *testing.T) {
	h := setupProjectHarness(t)
	p := h.seedProject("doomed", h.userID)

	resp := h.request("DELETE", "/api/v1/projects/"+p.ID, h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if body["message"] != "project deleted" {
		t.Errorf("expected message 'project deleted', got %v", body["message"])
	}

	// Confirm actually deleted
	resp = h.request("GET", "/api/v1/projects/"+p.ID, h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("project should be gone, got %d", resp.Code)
	}
}

func TestProjects_Delete_ForbiddenForNonOwner(t *testing.T) {
	h := setupProjectHarness(t)

	otherID := database.SeedTestUser(t, "other", "other@test.com")
	p := h.seedProject("others-proj", otherID)

	resp := h.request("DELETE", "/api/v1/projects/"+p.ID, h.userToken, nil)
	// Non-owner, non-admin user should not be able to access (404 from loadAndAuthorize)
	if resp.Code != http.StatusNotFound {
		t.Fatalf("expected 404 for other user's personal project, got %d", resp.Code)
	}
}

// ── Auth ──────────────────────────────────

func TestProjects_RequiresAuth(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("GET", "/api/v1/projects", "", nil)
	if resp.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401 without token, got %d", resp.Code)
	}
}

// ── Admin List ────────────────────────────

func TestProjects_AdminList_Envelope(t *testing.T) {
	h := setupProjectHarness(t)
	h.seedProject("admin-visible", h.userID)

	resp := h.request("GET", "/api/v1/admin/projects", h.adminToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.Unmarshal(resp.Body.Bytes(), &body)

	data, ok := body["data"].([]interface{})
	if !ok {
		t.Fatal("admin list must return {\"data\": [...]}")
	}
	if len(data) < 1 {
		t.Fatal("expected at least 1 project in admin list")
	}

	proj := data[0].(map[string]interface{})
	// Admin list has extra owner_name field
	if _, exists := proj["owner_name"]; !exists {
		t.Error("admin list project should include owner_name")
	}
	for _, key := range []string{"channel_count", "kb_count", "note_count"} {
		if _, exists := proj[key]; !exists {
			t.Errorf("admin list missing computed field %q", key)
		}
	}
}

func TestProjects_AdminList_ForbiddenForUser(t *testing.T) {
	h := setupProjectHarness(t)

	resp := h.request("GET", "/api/v1/admin/projects", h.userToken, nil)
	if resp.Code != http.StatusForbidden {
		t.Fatalf("expected 403 for non-admin, got %d", resp.Code)
	}
}
