package api

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

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

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

// newQuarantineRouter builds a test router for the user-facing /api/quarantine
// endpoints. Tests inject the user into context manually (RequireUser is
// applied in real Mount but skipped here so unit tests can target the handler
// path directly).
func newQuarantineRouter(h *handlers) chi.Router {
	r := chi.NewRouter()
	r.Post("/api/quarantine", h.handleFlag)
	r.Delete("/api/quarantine/{track_id}", h.handleUnflag)
	r.Get("/api/quarantine/mine", h.handleListMyQuarantine)
	return r
}

// doFlag fires POST /api/quarantine with body as the given user.
func doFlag(h *handlers, user dbq.User, body string) *httptest.ResponseRecorder {
	req := httptest.NewRequest(http.MethodPost, "/api/quarantine", bytes.NewBufferString(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, user)
	w := httptest.NewRecorder()
	newQuarantineRouter(h).ServeHTTP(w, req)
	return w
}

// doUnflag fires DELETE /api/quarantine/{track_id} as the given user.
func doUnflag(h *handlers, user dbq.User, trackID string) *httptest.ResponseRecorder {
	req := httptest.NewRequest(http.MethodDelete, "/api/quarantine/"+trackID, nil)
	req = withUser(req, user)
	w := httptest.NewRecorder()
	newQuarantineRouter(h).ServeHTTP(w, req)
	return w
}

// doListMine fires GET /api/quarantine/mine as the given user.
func doListMine(h *handlers, user dbq.User) *httptest.ResponseRecorder {
	req := httptest.NewRequest(http.MethodGet, "/api/quarantine/mine", nil)
	req = withUser(req, user)
	w := httptest.NewRecorder()
	newQuarantineRouter(h).ServeHTTP(w, req)
	return w
}

// seedQuarantineTrack creates a minimal artist+album+track for quarantine
// tests. Uniqueness is forced by appending unique into title fields.
func seedQuarantineTrack(t *testing.T, h *handlers, unique string) dbq.Track {
	t.Helper()
	artist := seedArtist(t, h.pool, "Q Artist "+unique)
	album := seedAlbum(t, h.pool, artist.ID, "Q Album "+unique, 0)
	return seedTrack(t, h.pool, album.ID, artist.ID, "Q Track "+unique, 1, 30000)
}

// TestFlag_HappyPath verifies POST /api/quarantine returns 201 and the row
// shows up in ListMine.
func TestFlag_HappyPath(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)
	track := seedQuarantineTrack(t, h, "happy")

	body := fmt.Sprintf(`{"track_id":%q,"reason":"bad_rip","notes":"crackly"}`, uuidToString(track.ID))
	w := doFlag(h, alice, body)

	if w.Code != http.StatusCreated {
		t.Fatalf("status = %d, want 201; body = %s", w.Code, w.Body.String())
	}
	var got quarantineView
	if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if got.Reason != "bad_rip" {
		t.Errorf("reason = %q, want bad_rip", got.Reason)
	}
	if got.Notes == nil || *got.Notes != "crackly" {
		t.Errorf("notes = %v, want 'crackly'", got.Notes)
	}
	if got.TrackID != uuidToString(track.ID) {
		t.Errorf("track_id = %q, want %q", got.TrackID, uuidToString(track.ID))
	}

	// Verify it shows up in ListMine.
	wm := doListMine(h, alice)
	if wm.Code != http.StatusOK {
		t.Fatalf("ListMine status = %d, body = %s", wm.Code, wm.Body.String())
	}
	var rows []quarantineMineView
	if err := json.Unmarshal(wm.Body.Bytes(), &rows); err != nil {
		t.Fatalf("decode mine: %v; body = %s", err, wm.Body.String())
	}
	if len(rows) != 1 {
		t.Fatalf("mine len = %d, want 1", len(rows))
	}
	if rows[0].TrackID != uuidToString(track.ID) {
		t.Errorf("mine track_id = %q, want %q", rows[0].TrackID, uuidToString(track.ID))
	}
	if rows[0].TrackTitle != track.Title {
		t.Errorf("mine track_title = %q, want %q", rows[0].TrackTitle, track.Title)
	}
}

// TestFlag_BadReason verifies an unknown reason returns 400 bad_reason.
func TestFlag_BadReason(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)
	track := seedQuarantineTrack(t, h, "badreason")

	body := fmt.Sprintf(`{"track_id":%q,"reason":"garbage"}`, uuidToString(track.ID))
	w := doFlag(h, alice, body)

	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; 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 != "bad_reason" {
		t.Errorf("error.code = %q, want bad_reason", resp.Error.Code)
	}
}

// TestFlag_TrackNotFound verifies a nonexistent track id returns 404.
func TestFlag_TrackNotFound(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)

	// Made-up but well-formed UUID — track won't exist in the DB.
	body := `{"track_id":"00000000-0000-0000-0000-000000000099","reason":"bad_rip"}`
	w := doFlag(h, alice, body)

	if w.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; 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 != "track_not_found" {
		t.Errorf("error.code = %q, want track_not_found", resp.Error.Code)
	}
}

// TestUnflag_HappyPath verifies DELETE returns 204 after a flag.
func TestUnflag_HappyPath(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)
	track := seedQuarantineTrack(t, h, "unflag")

	body := fmt.Sprintf(`{"track_id":%q,"reason":"bad_rip"}`, uuidToString(track.ID))
	if w := doFlag(h, alice, body); w.Code != http.StatusCreated {
		t.Fatalf("flag failed: %d, body = %s", w.Code, w.Body.String())
	}

	w := doUnflag(h, alice, uuidToString(track.ID))
	if w.Code != http.StatusNoContent {
		t.Fatalf("unflag status = %d, want 204; body = %s", w.Code, w.Body.String())
	}

	// Confirm it's gone.
	mine := doListMine(h, alice)
	var rows []quarantineMineView
	_ = json.Unmarshal(mine.Body.Bytes(), &rows)
	if len(rows) != 0 {
		t.Errorf("after unflag, mine len = %d, want 0", len(rows))
	}
}

// TestUnflag_NotFound verifies DELETE on an un-flagged track returns 404
// quarantine_not_found.
func TestUnflag_NotFound(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)
	track := seedQuarantineTrack(t, h, "unflagnf")

	w := doUnflag(h, alice, uuidToString(track.ID))
	if w.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404; 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 != "quarantine_not_found" {
		t.Errorf("error.code = %q, want quarantine_not_found", resp.Error.Code)
	}
}

// TestListMine_OK seeds two flags on alice and one on bob, verifies alice
// only sees her own.
func TestListMine_OK(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	alice := seedUser(t, pool, "alice", "pw", false)
	bob := seedUser(t, pool, "bob", "pw", false)

	t1 := seedQuarantineTrack(t, h, "lm-1")
	t2 := seedQuarantineTrack(t, h, "lm-2")
	t3 := seedQuarantineTrack(t, h, "lm-3")

	for _, body := range []string{
		fmt.Sprintf(`{"track_id":%q,"reason":"bad_rip"}`, uuidToString(t1.ID)),
		fmt.Sprintf(`{"track_id":%q,"reason":"wrong_tags"}`, uuidToString(t2.ID)),
	} {
		if w := doFlag(h, alice, body); w.Code != http.StatusCreated {
			t.Fatalf("alice flag: %d, body = %s", w.Code, w.Body.String())
		}
	}
	bobBody := fmt.Sprintf(`{"track_id":%q,"reason":"duplicate"}`, uuidToString(t3.ID))
	if w := doFlag(h, bob, bobBody); w.Code != http.StatusCreated {
		t.Fatalf("bob flag: %d, body = %s", w.Code, w.Body.String())
	}

	w := doListMine(h, alice)
	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
	}
	var rows []quarantineMineView
	if err := json.Unmarshal(w.Body.Bytes(), &rows); err != nil {
		t.Fatalf("decode: %v; body = %s", err, w.Body.String())
	}
	if len(rows) != 2 {
		t.Fatalf("len = %d, want 2 (alice's only)", len(rows))
	}
	for _, r := range rows {
		if r.TrackID == uuidToString(t3.ID) {
			t.Errorf("alice can see bob's flag")
		}
	}
}

// TestQuarantineEndpointsRequireAuth verifies the /api/quarantine endpoints
// return 401 when no user is in context (covers RequireUser failure mode
// uniformly across the three handlers).
func TestQuarantineEndpointsRequireAuth(t *testing.T) {
	h, pool := testHandlers(t)
	truncateLibrary(t, pool)

	cases := []struct {
		name   string
		method string
		path   string
		body   string
	}{
		{"flag", http.MethodPost, "/api/quarantine", `{"track_id":"00000000-0000-0000-0000-000000000001","reason":"bad_rip"}`},
		{"unflag", http.MethodDelete, "/api/quarantine/00000000-0000-0000-0000-000000000001", ""},
		{"mine", http.MethodGet, "/api/quarantine/mine", ""},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			var reqBody *bytes.Buffer
			if tc.body != "" {
				reqBody = bytes.NewBufferString(tc.body)
			} else {
				reqBody = bytes.NewBuffer(nil)
			}
			req := httptest.NewRequest(tc.method, tc.path, reqBody)
			if tc.body != "" {
				req.Header.Set("Content-Type", "application/json")
			}
			// No user injected into context.
			req = req.WithContext(context.Background())
			w := httptest.NewRecorder()
			newQuarantineRouter(h).ServeHTTP(w, req)
			if w.Code != http.StatusUnauthorized {
				t.Errorf("%s: status = %d, want 401; body = %s", tc.name, w.Code, w.Body.String())
			}
		})
	}
}
