package api

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

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

	"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
)

// newAdminLidarrRouter builds a test chi router with the five admin/lidarr
// endpoints. RequireAdmin middleware is applied. Tests inject a user into
// context directly (skipping RequireUser).
func newAdminLidarrRouter(h *handlers) chi.Router {
	r := chi.NewRouter()
	r.Route("/api/admin", func(admin chi.Router) {
		admin.Use(auth.RequireAdmin())
		admin.Get("/lidarr/config", h.handleGetLidarrConfig)
		admin.Put("/lidarr/config", h.handlePutLidarrConfig)
		admin.Post("/lidarr/test", h.handleTestLidarrConnection)
		admin.Get("/lidarr/quality-profiles", h.handleListQualityProfiles)
		admin.Get("/lidarr/root-folders", h.handleListRootFolders)
	})
	return r
}

// seedAdminUser creates a test user with is_admin=true.
func seedAdminUser(t *testing.T, h *handlers) dbq.User {
	t.Helper()
	return seedUser(t, h.pool, "admin", "pw", true)
}

// doAdminReq fires an HTTP request at the admin router with the given user
// injected into context (bypassing RequireUser).
func doAdminReq(t *testing.T, h *handlers, method, path string, body []byte, user dbq.User) *httptest.ResponseRecorder {
	t.Helper()
	var reqBody *bytes.Buffer
	if body != nil {
		reqBody = bytes.NewBuffer(body)
	} else {
		reqBody = bytes.NewBuffer(nil)
	}
	req := httptest.NewRequest(method, path, reqBody)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req = withUser(req, user)
	w := httptest.NewRecorder()
	newAdminLidarrRouter(h).ServeHTTP(w, req)
	return w
}

// saveLidarrConfigFull writes a lidarr config with the given key directly
// (for admin tests that need precise api_key control).
func saveLidarrConfigFull(t *testing.T, h *handlers, cfg lidarrconfig.Config) {
	t.Helper()
	svc := lidarrconfig.New(h.pool)
	if err := svc.Save(context.Background(), cfg); err != nil {
		t.Fatalf("saveLidarrConfigFull: %v", err)
	}
}

// TestHandleGetLidarrConfig_MasksAPIKey verifies that a set api_key is
// returned as "***" and never in plaintext.
func TestHandleGetLidarrConfig_MasksAPIKey(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: true,
		BaseURL: "http://lidarr.lan:8686",
		APIKey:  "secret123",
	})

	w := doAdminReq(t, h, http.MethodGet, "/api/admin/lidarr/config", nil, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp lidarrConfigView
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp.APIKey == "secret123" {
		t.Error("api_key leaked in response; want ***")
	}
	if resp.APIKey != "***" {
		t.Errorf("api_key = %q, want ***", resp.APIKey)
	}
}

// TestHandleGetLidarrConfig_EmptyKeyReturnsEmpty verifies that when api_key is
// unset in the DB, GET returns api_key="" (not "***").
func TestHandleGetLidarrConfig_EmptyKeyReturnsEmpty(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	// Save config with empty api_key (null in DB via lidarrconfig.Service).
	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: false,
		BaseURL: "",
		APIKey:  "",
	})

	w := doAdminReq(t, h, http.MethodGet, "/api/admin/lidarr/config", nil, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp lidarrConfigView
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp.APIKey != "" {
		t.Errorf("api_key = %q, want empty string", resp.APIKey)
	}
}

// TestHandlePutLidarrConfig_EmptyKeyPreservesSaved verifies that a PUT with
// api_key="" does not overwrite the saved api_key.
func TestHandlePutLidarrConfig_EmptyKeyPreservesSaved(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	// Seed a config with a known api_key.
	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: true,
		BaseURL: "http://lidarr.lan:8686",
		APIKey:  "originalkey",
	})

	// PUT with empty api_key — should preserve "originalkey". enabled=true
	// requires the defaults gate (missing_defaults) to be satisfied.
	body := []byte(`{"enabled":true,"base_url":"http://lidarr.lan:8686","api_key":"","default_quality_profile_id":1,"default_metadata_profile_id":1,"default_root_folder_path":"/music"}`)
	w := doAdminReq(t, h, http.MethodPut, "/api/admin/lidarr/config", body, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}

	// Confirm the saved api_key is still "originalkey" by reading raw config.
	svc := lidarrconfig.New(h.pool)
	saved, err := svc.Get(context.Background())
	if err != nil {
		t.Fatalf("get saved config: %v", err)
	}
	if saved.APIKey != "originalkey" {
		t.Errorf("saved api_key = %q, want originalkey", saved.APIKey)
	}
}

// TestHandlePutLidarrConfig_EnabledRequiresBaseURL_400 verifies that enabling
// Lidarr without a base_url returns 400 with missing_required_field.
func TestHandlePutLidarrConfig_EnabledRequiresBaseURL_400(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	body := []byte(`{"enabled":true,"base_url":"","api_key":"somekey"}`)
	w := doAdminReq(t, h, http.MethodPut, "/api/admin/lidarr/config", body, admin)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body = %s", w.Code, w.Body.String())
	}
	var resp map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp["error"] != "missing_required_field" {
		t.Errorf("error = %q, want missing_required_field", resp["error"])
	}
}

// TestHandlePutLidarrConfig_HappyPath verifies a valid PUT returns 200 with
// the updated config and the api_key masked.
func TestHandlePutLidarrConfig_HappyPath(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	body := []byte(`{"enabled":true,"base_url":"http://lidarr.lan:8686","api_key":"newkey","default_quality_profile_id":2,"default_metadata_profile_id":1,"default_root_folder_path":"/music"}`)
	w := doAdminReq(t, h, http.MethodPut, "/api/admin/lidarr/config", body, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp lidarrConfigView
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if !resp.Enabled {
		t.Error("enabled = false, want true")
	}
	if resp.BaseURL != "http://lidarr.lan:8686" {
		t.Errorf("base_url = %q, want http://lidarr.lan:8686", resp.BaseURL)
	}
	if resp.APIKey != "***" {
		t.Errorf("api_key = %q, want ***", resp.APIKey)
	}
	if resp.DefaultQualityProfileID != 2 {
		t.Errorf("default_quality_profile_id = %d, want 2", resp.DefaultQualityProfileID)
	}
	if resp.DefaultRootFolderPath != "/music" {
		t.Errorf("default_root_folder_path = %q, want /music", resp.DefaultRootFolderPath)
	}
}

// TestHandleTestLidarrConnection_HappyPath verifies that POST /test against a
// stub returning valid status JSON responds 200 with ok=true and the version.
func TestHandleTestLidarrConnection_HappyPath(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/api/v1/system/status":
			_, _ = w.Write([]byte(`{"version":"2.0.5"}`))
		case "/api/v1/qualityprofile":
			_, _ = w.Write([]byte(`[{"id":1,"name":"FLAC"},{"id":2,"name":"MP3-320"}]`))
		case "/api/v1/metadataprofile":
			_, _ = w.Write([]byte(`[{"id":1,"name":"Standard"}]`))
		case "/api/v1/rootfolder":
			_, _ = w.Write([]byte(`[{"path":"/music","accessible":true,"freeSpace":12345}]`))
		default:
			http.NotFound(w, r)
		}
	}))
	t.Cleanup(stub.Close)

	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: true,
		BaseURL: stub.URL,
		APIKey:  "test-key",
	})

	// POST with empty body — falls back to saved config.
	w := doAdminReq(t, h, http.MethodPost, "/api/admin/lidarr/test", []byte(`{}`), admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp["ok"] != true {
		t.Errorf("ok = %v, want true", resp["ok"])
	}
	if resp["version"] != "2.0.5" {
		t.Errorf("version = %v, want 2.0.5", resp["version"])
	}
	qp, ok := resp["quality_profiles"].([]any)
	if !ok || len(qp) != 2 {
		t.Errorf("quality_profiles = %v, want 2 entries", resp["quality_profiles"])
	}
	mp, ok := resp["metadata_profiles"].([]any)
	if !ok || len(mp) != 1 {
		t.Errorf("metadata_profiles = %v, want 1 entry", resp["metadata_profiles"])
	}
	rf, ok := resp["root_folders"].([]any)
	if !ok || len(rf) != 1 {
		t.Errorf("root_folders = %v, want 1 entry", resp["root_folders"])
	}
	if _, present := resp["list_errors"]; present {
		t.Errorf("list_errors should be omitted when all lists succeed; got %v", resp["list_errors"])
	}
}

// TestHandleTestLidarrConnection_PartialListFailure verifies that POST /test
// against a Lidarr that responds OK to the ping + two of three lists still
// reports ok=true with the failing list omitted and named in list_errors.
func TestHandleTestLidarrConnection_PartialListFailure(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.URL.Path {
		case "/api/v1/system/status":
			_, _ = w.Write([]byte(`{"version":"2.0.5"}`))
		case "/api/v1/qualityprofile":
			_, _ = w.Write([]byte(`[{"id":1,"name":"FLAC"}]`))
		case "/api/v1/metadataprofile":
			http.Error(w, "boom", http.StatusInternalServerError)
		case "/api/v1/rootfolder":
			_, _ = w.Write([]byte(`[{"path":"/music","accessible":true,"freeSpace":12345}]`))
		default:
			http.NotFound(w, r)
		}
	}))
	t.Cleanup(stub.Close)

	body := []byte(`{"base_url":"` + stub.URL + `","api_key":"k"}`)
	w := doAdminReq(t, h, http.MethodPost, "/api/admin/lidarr/test", body, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp["ok"] != true {
		t.Errorf("ok = %v, want true", resp["ok"])
	}
	if _, present := resp["metadata_profiles"]; present {
		t.Errorf("metadata_profiles should be omitted on failure; got %v", resp["metadata_profiles"])
	}
	listErrs, ok := resp["list_errors"].(map[string]any)
	if !ok {
		t.Fatalf("list_errors missing or wrong shape: %v", resp["list_errors"])
	}
	if _, present := listErrs["metadata_profiles"]; !present {
		t.Errorf("list_errors.metadata_profiles should be present, got %v", listErrs)
	}
}

// TestHandleTestLidarrConnection_Unreachable verifies that POST /test against
// an unreachable URL returns 200 with ok=false and lidarr_unreachable.
func TestHandleTestLidarrConnection_Unreachable(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	// Point at a URL that will immediately refuse connections.
	body := []byte(`{"base_url":"http://127.0.0.1:1","api_key":"k"}`)
	w := doAdminReq(t, h, http.MethodPost, "/api/admin/lidarr/test", body, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if resp["ok"] != false {
		t.Errorf("ok = %v, want false", resp["ok"])
	}
	if resp["error"] != "lidarr_unreachable" {
		t.Errorf("error = %v, want lidarr_unreachable", resp["error"])
	}
}

// TestHandleListQualityProfiles_HappyPath verifies that GET /quality-profiles
// proxies and normalizes the Lidarr response.
func TestHandleListQualityProfiles_HappyPath(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	stub := newLidarrStub(t, `[{"id":1,"name":"Lossless"},{"id":2,"name":"Standard"}]`)
	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: true,
		BaseURL: stub.URL,
		APIKey:  "test-key",
	})

	w := doAdminReq(t, h, http.MethodGet, "/api/admin/lidarr/quality-profiles", nil, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var profiles []qualityProfileView
	if err := json.Unmarshal(w.Body.Bytes(), &profiles); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if len(profiles) != 2 {
		t.Fatalf("len(profiles) = %d, want 2", len(profiles))
	}
	if profiles[0].ID != 1 || profiles[0].Name != "Lossless" {
		t.Errorf("profiles[0] = %+v, want {1 Lossless}", profiles[0])
	}
	if profiles[1].ID != 2 || profiles[1].Name != "Standard" {
		t.Errorf("profiles[1] = %+v, want {2 Standard}", profiles[1])
	}
}

// TestHandleListRootFolders_HappyPath verifies that GET /root-folders proxies
// and normalizes the Lidarr response.
func TestHandleListRootFolders_HappyPath(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)
	admin := seedAdminUser(t, h)

	stub := newLidarrStub(t, `[{"path":"/music","accessible":true,"freeSpace":1234567890}]`)
	saveLidarrConfigFull(t, h, lidarrconfig.Config{
		Enabled: true,
		BaseURL: stub.URL,
		APIKey:  "test-key",
	})

	w := doAdminReq(t, h, http.MethodGet, "/api/admin/lidarr/root-folders", nil, admin)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var folders []rootFolderView
	if err := json.Unmarshal(w.Body.Bytes(), &folders); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if len(folders) != 1 {
		t.Fatalf("len(folders) = %d, want 1", len(folders))
	}
	f := folders[0]
	if f.Path != "/music" {
		t.Errorf("path = %q, want /music", f.Path)
	}
	if !f.Accessible {
		t.Error("accessible = false, want true")
	}
	if f.FreeSpace != 1234567890 {
		t.Errorf("free_space = %d, want 1234567890", f.FreeSpace)
	}
}

// TestAdminEndpoints_NonAdmin403 is a table-driven test verifying that every
// admin endpoint returns 403 with not_authorized for a non-admin user.
func TestAdminEndpoints_NonAdmin403(t *testing.T) {
	h, pool := testHandlers(t)
	resetLidarrState(t, h)

	nonAdmin := seedUser(t, pool, "regular", "pw", false)

	cases := []struct {
		method string
		path   string
		body   []byte
	}{
		{http.MethodGet, "/api/admin/lidarr/config", nil},
		{http.MethodPut, "/api/admin/lidarr/config", []byte(`{}`)},
		{http.MethodPost, "/api/admin/lidarr/test", []byte(`{}`)},
		{http.MethodGet, "/api/admin/lidarr/quality-profiles", nil},
		{http.MethodGet, "/api/admin/lidarr/root-folders", nil},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.method+" "+tc.path, func(t *testing.T) {
			w := doAdminReq(t, h, tc.method, tc.path, tc.body, nonAdmin)
			if w.Code != http.StatusForbidden {
				t.Fatalf("status = %d, want 403; body = %s", w.Code, w.Body.String())
			}
			var resp map[string]string
			if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
				t.Fatalf("decode: %v; body = %s", err, w.Body.String())
			}
			if resp["error"] != "not_authorized" {
				t.Errorf("error = %q, want not_authorized", resp["error"])
			}
		})
	}
}
