package api

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

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

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

// apiTestTagProvider is a minimal TrackTagProvider for api-package tests.
// Does not implement TestableProvider.
type apiTestTagProvider struct {
	id      string
	display string
}

func (p *apiTestTagProvider) ID() string                              { return p.id }
func (p *apiTestTagProvider) DisplayName() string                     { return p.display }
func (p *apiTestTagProvider) RequiresAPIKey() bool                    { return false }
func (p *apiTestTagProvider) DefaultEnabled() bool                    { return true }
func (p *apiTestTagProvider) Configure(_ tags.ProviderSettings) error { return nil }
func (p *apiTestTagProvider) FetchTrackTags(_ context.Context, _ tags.TrackRef) ([]tags.Tag, error) {
	return []tags.Tag{{Name: "x", Weight: 1}}, nil
}

// apiTestTestableTagProvider also implements TestableProvider.
type apiTestTestableTagProvider struct {
	apiTestTagProvider
}

func (p *apiTestTestableTagProvider) TestConnection(_ context.Context) error { return nil }

func newAdminTagSourcesRouter(h *handlers) chi.Router {
	r := chi.NewRouter()
	r.Route("/api/admin", func(admin chi.Router) {
		admin.Use(auth.RequireAdmin())
		admin.Get("/tag-sources", h.handleListTagSources)
		admin.Patch("/tag-sources/{provider_id}", h.handleUpdateTagSource)
		admin.Post("/tag-sources/{provider_id}/test", h.handleTestTagSource)
	})
	return r
}

// testHandlersWithTagSettings installs a tags.SettingsService backed by the
// test DB. Register the needed fake providers BEFORE calling this, and pair
// with tags.ResetRegistryForTests in t.Cleanup.
func testHandlersWithTagSettings(t *testing.T) *handlers {
	t.Helper()
	h, pool := testHandlers(t)
	s, err := tags.NewSettingsService(context.Background(), pool, h.logger)
	if err != nil {
		t.Fatalf("NewSettingsService: %v", err)
	}
	h.tagSettings = s
	return h
}

func TestAdminListTagSources_ReturnsRegisteredProviders(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)
	tags.Register(&apiTestTagProvider{id: "test-tag-prov", display: "Test Tag Provider"})

	h := testHandlersWithTagSettings(t)
	admin := seedUser(t, h.pool, "tslist", "pw", true)

	req := httptest.NewRequest(http.MethodGet, "/api/admin/tag-sources", nil)
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp tagSourcesListResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if len(resp.Providers) != 1 || resp.Providers[0].ID != "test-tag-prov" {
		t.Fatalf("providers = %+v, want 1 (test-tag-prov)", resp.Providers)
	}
	found := false
	for _, s := range resp.Providers[0].Supports {
		if s == "track_tags" {
			found = true
		}
	}
	if !found {
		t.Errorf("supports = %v, want to include track_tags", resp.Providers[0].Supports)
	}
	if resp.Providers[0].Testable {
		t.Error("testable = true, want false (no TestConnection)")
	}
}

func TestAdminUpdateTagSource_FlippingEnabledBumpsVersion(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)
	tags.Register(&apiTestTagProvider{id: "flip-tag", display: "Flip Tag"})

	h := testHandlersWithTagSettings(t)
	admin := seedUser(t, h.pool, "tsflip", "pw", true)

	disabled := false
	body, _ := json.Marshal(updateTagSourceReq{Enabled: &disabled})
	req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/flip-tag", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp updateTagSourceResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !resp.VersionBumped {
		t.Error("version_bumped = false, want true after flipping enabled")
	}
}

func TestAdminUpdateTagSource_KeyOnlyDoesNotBump(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)
	tags.Register(&apiTestTagProvider{id: "key-tag", display: "Key Tag"})

	h := testHandlersWithTagSettings(t)
	admin := seedUser(t, h.pool, "tskey", "pw", true)

	newKey := "newkey"
	body, _ := json.Marshal(updateTagSourceReq{APIKey: &newKey})
	req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/key-tag", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp updateTagSourceResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.VersionBumped {
		t.Error("version_bumped = true, want false for key-only change")
	}
	if !resp.APIKeySet {
		t.Error("api_key_set = false, want true after setting key")
	}
}

func TestAdminUpdateTagSource_UnknownProvider404(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)

	h := testHandlersWithTagSettings(t)
	admin := seedUser(t, h.pool, "ts404", "pw", true)

	body, _ := json.Marshal(updateTagSourceReq{})
	req := httptest.NewRequest(http.MethodPatch, "/api/admin/tag-sources/missing", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

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

func TestAdminTestTagSource_NonAdminReturns403(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)
	tags.Register(&apiTestTestableTagProvider{apiTestTagProvider{id: "testable-tag", display: "Testable Tag"}})

	h := testHandlersWithTagSettings(t)
	nonAdmin := seedUser(t, h.pool, "tsnoadmin", "pw", false)

	req := httptest.NewRequest(http.MethodPost, "/api/admin/tag-sources/testable-tag/test", nil)
	req = withUser(req, nonAdmin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

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

func TestAdminTestTagSource_NotTestableReturnsOkFalse(t *testing.T) {
	if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	tags.ResetRegistryForTests()
	t.Cleanup(tags.ResetRegistryForTests)
	tags.Register(&apiTestTagProvider{id: "nontestable-tag", display: "Non-Testable Tag"})

	h := testHandlersWithTagSettings(t)
	admin := seedUser(t, h.pool, "tsntest", "pw", true)

	req := httptest.NewRequest(http.MethodPost, "/api/admin/tag-sources/nontestable-tag/test", nil)
	req = withUser(req, admin)
	rec := httptest.NewRecorder()
	newAdminTagSourcesRouter(h).ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp testTagSourceResp
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.OK {
		t.Error("ok = true, want false for non-testable provider")
	}
	if resp.Error == "" {
		t.Error("error string empty, want a message")
	}
}
