package api

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

	"github.com/go-chi/chi/v5"

	"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
)

// newAdminCoverageRouter builds a test chi router exposing only the
// coverage endpoint, mirroring the pattern in admin_scan_test.go.
func newAdminCoverageRouter(h *handlers) chi.Router {
	r := chi.NewRouter()
	r.Route("/api/admin", func(admin chi.Router) {
		admin.Use(auth.RequireAdmin())
		admin.Get("/library/coverage", h.handleGetLibraryCoverage)
	})
	return r
}

func TestAdminLibraryCoverage_EmptyLibraryReturnsZeros(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	admin := seedUser(t, pool, "covgauge1", "pw", true)

	req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminCoverageRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp coverageRollupResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Total != 0 || resp.WithArt != 0 || resp.Pending != 0 || resp.Settled != 0 || resp.PendingNoMbid != 0 {
		t.Errorf("expected all zeros on empty library; got %+v", resp)
	}
}

func TestAdminLibraryCoverage_MixedRowsReturnCorrectBuckets(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	admin := seedUser(t, pool, "covgauge2", "pw", true)

	// Seed an artist for FK satisfaction.
	artist := seedArtist(t, pool, "Coverage Test")

	// Seed albums covering each bucket. Use distinct titles to avoid the
	// unique (artist_id, title) constraint. Distinct mbids where set so
	// the partial unique index on mbid doesn't trip.
	type seed struct {
		title  string
		source *string
		mbid   *string
	}
	sourceNone := "none"
	sourceSidecar := "sidecar"
	sourceMbcaa := "mbcaa"
	sourceEmbedded := "embedded"
	sourceTheaudiodb := "theaudiodb"
	sourceDeezer := "deezer"
	sourceLastfm := "lastfm"
	mbid1 := "11111111-1111-1111-1111-111111111111"
	mbid2 := "22222222-2222-2222-2222-222222222222"
	mbid3 := "33333333-3333-3333-3333-333333333333"
	mbid4 := "44444444-4444-4444-4444-444444444444"
	mbid5 := "55555555-5555-5555-5555-555555555555"
	mbid6 := "66666666-6666-6666-6666-666666666666"
	mbid7 := "77777777-7777-7777-7777-777777777777"
	// Cover every cover_art_source value the migrations allow so a future
	// addition without updating the rollup query trips this test —
	// regression guard for drift #557, the gap that hid #556 (deezer +
	// lastfm omission introduced by migration 0020 but never wired into
	// the rollup whitelist).
	rows := []seed{
		{title: "WithArtSidecar", source: &sourceSidecar, mbid: &mbid1},       // with_art
		{title: "WithArtMbcaa", source: &sourceMbcaa, mbid: &mbid2},           // with_art
		{title: "WithArtEmbedded", source: &sourceEmbedded, mbid: &mbid4},     // with_art
		{title: "WithArtTheaudiodb", source: &sourceTheaudiodb, mbid: &mbid5}, // with_art
		{title: "WithArtDeezer", source: &sourceDeezer, mbid: &mbid6},         // with_art
		{title: "WithArtLastfm", source: &sourceLastfm, mbid: &mbid7},         // with_art
		{title: "PendingHasMbid", source: nil, mbid: &mbid3},                  // pending (eligible)
		{title: "PendingNoMbid", source: nil, mbid: nil},                      // pending + pending_no_mbid
		{title: "Settled", source: &sourceNone, mbid: nil},                    // settled (no mbid is fine here)
	}
	for _, s := range rows {
		if _, err := pool.Exec(context.Background(), `
			INSERT INTO albums (artist_id, title, sort_title, cover_art_source, mbid)
			VALUES ($1, $2, $3, $4, $5)
		`, artist.ID, s.title, s.title, s.source, s.mbid); err != nil {
			t.Fatalf("seed album %q: %v", s.title, err)
		}
	}

	req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminCoverageRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp coverageRollupResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Total != 9 {
		t.Errorf("Total = %d, want 9", resp.Total)
	}
	// Six with_art rows — one per valid cover_art_source value
	// (sidecar, mbcaa, embedded, theaudiodb, deezer, lastfm).
	if resp.WithArt != 6 {
		t.Errorf("WithArt = %d, want 6", resp.WithArt)
	}
	if resp.Pending != 2 {
		t.Errorf("Pending = %d, want 2", resp.Pending)
	}
	if resp.Settled != 1 {
		t.Errorf("Settled = %d, want 1", resp.Settled)
	}
	if resp.PendingNoMbid != 1 {
		t.Errorf("PendingNoMbid = %d, want 1", resp.PendingNoMbid)
	}
	// Invariant: with_art + pending + settled == total.
	if resp.WithArt+resp.Pending+resp.Settled != resp.Total {
		t.Errorf("invariant violated: %d + %d + %d != %d",
			resp.WithArt, resp.Pending, resp.Settled, resp.Total)
	}
}

func TestAdminLibraryCoverage_NonAdminReturns403(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	h, pool := testHandlers(t)
	user := seedUser(t, pool, "covgauge3", "pw", false)

	req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
	req = withUser(req, user)
	rec := httptest.NewRecorder()
	newAdminCoverageRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403; body=%s", rec.Code, rec.Body.String())
	}
}
