package api

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgtype"

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

// resetLidarrState clears lidarr_requests rows and resets lidarr_config
// to the factory-default state (enabled=false, nulls) between tests.
func resetLidarrState(t *testing.T, h *handlers) {
	t.Helper()
	ctx := context.Background()
	if _, err := h.pool.Exec(ctx,
		"DELETE FROM lidarr_requests; UPDATE lidarr_config SET enabled=false, base_url=NULL, api_key=NULL WHERE id=1",
	); err != nil {
		t.Fatalf("resetLidarrState: %v", err)
	}
}

// saveLidarrConfig writes a test lidarr_config row pointing at the given stub URL.
func saveLidarrConfig(t *testing.T, h *handlers, baseURL string, enabled bool) {
	t.Helper()
	svc := lidarrconfig.New(h.pool)
	err := svc.Save(context.Background(), lidarrconfig.Config{
		Enabled:                 enabled,
		BaseURL:                 baseURL,
		APIKey:                  "test-key",
		DefaultQualityProfileID: 1,
		DefaultRootFolderPath:   "/music",
	})
	if err != nil {
		t.Fatalf("saveLidarrConfig: %v", err)
	}
}

// lidarrArtistStubBody returns a minimal Lidarr artist lookup JSON payload.
func lidarrArtistStubBody(mbid, name string) string {
	return fmt.Sprintf(`[{"foreignArtistId":%q,"artistName":%q,"genres":["Rock"],"albumCount":5,"images":[]}]`, mbid, name)
}

// newLidarrStub creates an httptest.Server that always returns the given body
// with 200 OK for any /api/v1/* path.
func newLidarrStub(t *testing.T, body string) *httptest.Server {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(srv.Close)
	return srv
}

// seedArtistWithMBID inserts an artist row with a specific MBID for in_library testing.
func seedArtistWithMBID(t *testing.T, h *handlers, name, mbid string) dbq.Artist {
	t.Helper()
	a, err := dbq.New(h.pool).UpsertArtist(context.Background(), dbq.UpsertArtistParams{
		Name:     name,
		SortName: name,
		Mbid:     &mbid,
	})
	if err != nil {
		t.Fatalf("seedArtistWithMBID: %v", err)
	}
	return a
}

// seedRequestForMBID inserts a pending lidarr_requests row for the given MBID.
func seedRequestForMBID(t *testing.T, h *handlers, userID pgtype.UUID, kind dbq.LidarrRequestKind, artistMBID, mbid string) {
	t.Helper()
	var albumMBID, trackMBID *string
	switch kind {
	case dbq.LidarrRequestKindAlbum:
		albumMBID = &mbid
	case dbq.LidarrRequestKindTrack:
		trackMBID = &mbid
	}
	_, err := dbq.New(h.pool).CreateLidarrRequest(context.Background(), dbq.CreateLidarrRequestParams{
		UserID:           userID,
		Kind:             kind,
		LidarrArtistMbid: artistMBID,
		LidarrAlbumMbid:  albumMBID,
		LidarrTrackMbid:  trackMBID,
		ArtistName:       "Test Artist",
		AlbumTitle:       nil,
		TrackTitle:       nil,
	})
	if err != nil {
		t.Fatalf("seedRequestForMBID: %v", err)
	}
}

// doLidarrSearch fires a GET /api/lidarr/search request directly at the handler.
// It simulates the RequireUser middleware by injecting a dummy user context.
func doLidarrSearch(t *testing.T, h *handlers, q, kind string) *httptest.ResponseRecorder {
	t.Helper()
	url := fmt.Sprintf("/api/lidarr/search?q=%s&kind=%s", q, kind)
	req := httptest.NewRequest(http.MethodGet, url, nil)
	// Inject a minimal user context the same way other handler tests do it.
	// The lidarr search handler doesn't use the user from context, so any
	// non-nil user value keeps the handler from panicking.
	w := httptest.NewRecorder()
	h.handleLidarrSearch(w, req)
	return w
}

// TestHandleLidarrSearch_ValidationErrors covers bad kind and missing query
// in a table-driven test.
func TestHandleLidarrSearch_ValidationErrors(t *testing.T) {
	h, _ := testHandlers(t)

	cases := []struct {
		name       string
		q          string
		kind       string
		wantStatus int
		wantCode   string
	}{
		{
			name:       "bad kind playlist",
			q:          "Beatles",
			kind:       "playlist",
			wantStatus: http.StatusBadRequest,
			wantCode:   "bad_kind",
		},
		{
			name:       "missing query",
			q:          "",
			kind:       "artist",
			wantStatus: http.StatusBadRequest,
			wantCode:   "missing_query",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			url := fmt.Sprintf("/api/lidarr/search?q=%s&kind=%s", tc.q, tc.kind)
			req := httptest.NewRequest(http.MethodGet, url, nil)
			w := httptest.NewRecorder()
			h.handleLidarrSearch(w, req)

			if w.Code != tc.wantStatus {
				t.Fatalf("status = %d, want %d; body = %s", w.Code, tc.wantStatus, w.Body.String())
			}
			var resp struct {
				Error struct {
					Code string `json:"code"`
				} `json:"error"`
			}
			if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
				t.Fatalf("decode body: %v; body=%s", err, w.Body.String())
			}
			if resp.Error.Code != tc.wantCode {
				t.Errorf("error.code = %q, want %q", resp.Error.Code, tc.wantCode)
			}
		})
	}
}

// TestHandleLidarrSearch_DisabledReturns503 verifies that a disabled Lidarr
// config causes the handler to return 503 lidarr_disabled.
func TestHandleLidarrSearch_DisabledReturns503(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)

	// Config is already disabled after resetLidarrState; ensure it's explicit.
	saveLidarrConfig(t, h, "http://127.0.0.1:1", false)

	req := httptest.NewRequest(http.MethodGet, "/api/lidarr/search?q=Beatles&kind=artist", nil)
	w := httptest.NewRecorder()
	h.handleLidarrSearch(w, req)

	if w.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body = %s", w.Code, w.Body.String())
	}
	var resp struct {
		Error struct {
			Code string `json:"code"`
		} `json:"error"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode body: %v; body=%s", err, w.Body.String())
	}
	if resp.Error.Code != "lidarr_disabled" {
		t.Errorf("error.code = %q, want lidarr_disabled", resp.Error.Code)
	}
}

// TestHandleLidarrSearch_HappyPath_RequestableResult checks a result that is
// neither in the library nor already requested.
func TestHandleLidarrSearch_HappyPath_RequestableResult(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)

	const mbid = "artist-mbid-abc123"
	stub := newLidarrStub(t, lidarrArtistStubBody(mbid, "The Beatles"))
	saveLidarrConfig(t, h, stub.URL, true)

	w := doLidarrSearch(t, h, "Beatles", "artist")

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var results []lidarrSearchResult
	if err := json.Unmarshal(w.Body.Bytes(), &results); err != nil {
		t.Fatalf("decode: %v; body=%s", err, w.Body.String())
	}
	if len(results) != 1 {
		t.Fatalf("len(results) = %d, want 1", len(results))
	}
	r := results[0]
	if r.MBID != mbid {
		t.Errorf("mbid = %q, want %q", r.MBID, mbid)
	}
	if r.InLibrary {
		t.Errorf("in_library = true, want false")
	}
	if r.Requested {
		t.Errorf("requested = true, want false")
	}
}

// TestHandleLidarrSearch_HappyPath_InLibraryResult checks that a result whose
// MBID matches an artist in the local DB has in_library=true.
func TestHandleLidarrSearch_HappyPath_InLibraryResult(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)

	const mbid = "artist-mbid-inlib"
	stub := newLidarrStub(t, lidarrArtistStubBody(mbid, "Library Artist"))
	saveLidarrConfig(t, h, stub.URL, true)

	// Seed the artist with the matching MBID so in_library becomes true.
	seedArtistWithMBID(t, h, dbtest.TestUserPrefix+"LibraryArtist", mbid)

	w := doLidarrSearch(t, h, "Library+Artist", "artist")

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var results []lidarrSearchResult
	if err := json.Unmarshal(w.Body.Bytes(), &results); err != nil {
		t.Fatalf("decode: %v; body=%s", err, w.Body.String())
	}
	if len(results) != 1 {
		t.Fatalf("len(results) = %d, want 1", len(results))
	}
	if !results[0].InLibrary {
		t.Errorf("in_library = false, want true")
	}
}

// TestHandleLidarrSearch_HappyPath_AlreadyRequested verifies that a result
// whose MBID matches a pending lidarr_requests row has requested=true.
func TestHandleLidarrSearch_HappyPath_AlreadyRequested(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)

	const mbid = "artist-mbid-requested"
	stub := newLidarrStub(t, lidarrArtistStubBody(mbid, "Requested Artist"))
	saveLidarrConfig(t, h, stub.URL, true)

	// Seed a user and a pending request for the artist MBID.
	user := seedUser(t, h.pool, "requester", "pw", false)
	seedRequestForMBID(t, h, user.ID, dbq.LidarrRequestKindArtist, mbid, mbid)

	w := doLidarrSearch(t, h, "Requested+Artist", "artist")

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
	}
	var results []lidarrSearchResult
	if err := json.Unmarshal(w.Body.Bytes(), &results); err != nil {
		t.Fatalf("decode: %v; body=%s", err, w.Body.String())
	}
	if len(results) != 1 {
		t.Fatalf("len(results) = %d, want 1", len(results))
	}
	if !results[0].Requested {
		t.Errorf("requested = false, want true")
	}
}

// TestHandleLidarrSearch_LidarrUnreachableReturns503 verifies that a
// connection-refused Lidarr base URL returns 503 lidarr_unreachable.
func TestHandleLidarrSearch_LidarrUnreachableReturns503(t *testing.T) {
	h, _ := testHandlers(t)
	resetLidarrState(t, h)

	// Port 1 is reserved and will refuse connections immediately.
	saveLidarrConfig(t, h, "http://127.0.0.1:1", true)

	req := httptest.NewRequest(http.MethodGet, "/api/lidarr/search?q=Beatles&kind=artist", nil)
	// Use a short-deadline context so the test doesn't hang waiting for TCP.
	ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
	defer cancel()
	req = req.WithContext(ctx)

	w := httptest.NewRecorder()
	h.handleLidarrSearch(w, req)

	if w.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503; body = %s", w.Code, w.Body.String())
	}
	var resp struct {
		Error struct {
			Code string `json:"code"`
		} `json:"error"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v; body=%s", err, w.Body.String())
	}
	if resp.Error.Code != "lidarr_unreachable" {
		t.Errorf("error.code = %q, want lidarr_unreachable", resp.Error.Code)
	}
}
