package web_test

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/stretchr/testify/require"

	"git.d-ma.be/mathias/tapir/internal/web"
)

// fakeProcessor records ProcessVideo calls. With block set it parks until the
// channel is closed, so a test can observe the handler return before the
// background work finishes (proving it ran in a goroutine).
type fakeProcessor struct {
	block chan struct{}
	done  chan struct{}
	calls []string
}

func (f *fakeProcessor) ProcessVideo(_ context.Context, _, videoID string) error {
	if f.block != nil {
		<-f.block
	}
	f.calls = append(f.calls, videoID)
	if f.done != nil {
		close(f.done)
	}
	return nil
}

func TestRequestSummarizeImmediateProcessing(t *testing.T) {
	app := newApp(t)
	p := rawPool(t)
	resetDB(t, p)
	seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})

	fp := &fakeProcessor{block: make(chan struct{}), done: make(chan struct{})}
	app.Processor = fp

	rec := postSummarize(t, app, videoX, true)
	require.Equal(t, http.StatusOK, rec.Code)
	html := body(t, rec)

	// The processing card came back while ProcessVideo is still parked on block:
	// the work runs in a goroutine, the handler did not wait for it.
	require.Contains(t, html, "Summarizing", "processing card returned")
	require.Contains(t, html, "╭", "charm box rendered")
	require.Contains(t, html, "▓", "tapir body block chars rendered")
	require.Contains(t, html, "∩", "wiggling snout frame rendered")
	require.Contains(t, html, web.CharmPurple, "charm palette applied to the border")
	require.Contains(t, html, "/v/"+videoX+"/status", "card polls the status endpoint")
	require.Contains(t, html, `hx-trigger="every 2s"`, "card auto-polls every 2s")
	require.NotContains(t, html, "Queued", "not the queue-only card")

	close(fp.block)
	select {
	case <-fp.done:
	case <-time.After(2 * time.Second):
		t.Fatal("ProcessVideo was not called in the background")
	}
	require.Equal(t, []string{videoX}, fp.calls)
}

func TestStatusProcessingThenDone(t *testing.T) {
	ctx := context.Background()
	app := newApp(t)
	p := rawPool(t)
	resetDB(t, p)
	seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})

	// Park ProcessVideo so the video stays in-flight while we poll status.
	fp := &fakeProcessor{block: make(chan struct{})}
	app.Processor = fp
	require.Equal(t, http.StatusOK, postSummarize(t, app, videoX, true).Code)

	// Processing: status returns the animation card, still polling.
	rec := getStatus(t, app, videoX)
	require.Equal(t, http.StatusOK, rec.Code)
	html := body(t, rec)
	require.Contains(t, html, "Summarizing", "in-flight → animation card")
	require.Contains(t, html, `hx-trigger="every 2s"`, "still polling")

	close(fp.block)

	// Done: once a summary exists, status returns the summary card with no poll.
	require.NoError(t, deliver(ctx, app, videoX, "the summary body"))
	rec = getStatus(t, app, videoX)
	require.Equal(t, http.StatusOK, rec.Code)
	html = body(t, rec)
	require.NotContains(t, html, "Summarizing", "done → no animation")
	require.NotContains(t, html, "every 2s", "done card does not poll (polling stops)")
	require.Contains(t, html, "/v/"+videoX+"\"", "links to the detail page")
}

func TestStatusQueuedWhenNotInFlight(t *testing.T) {
	ctx := context.Background()
	app := newApp(t)
	p := rawPool(t)
	resetDB(t, p)
	seedVideo(t, p, videoX, "Pending Title", "https://x", time.Time{})

	// Flag set but nothing in-flight (e.g. queue-only, or after a restart).
	require.NoError(t, app.Store.RequestSummarize(ctx, userID, videoX))

	rec := getStatus(t, app, videoX)
	require.Equal(t, http.StatusOK, rec.Code)
	html := body(t, rec)
	require.Contains(t, html, "Queued", "queued chip card")
	require.NotContains(t, html, "Summarizing", "not processing")
	require.NotContains(t, html, "every 2s", "queued card does not poll")
}

func TestStatusNotFound(t *testing.T) {
	app := newApp(t)
	resetDB(t, rawPool(t))
	rec := getStatus(t, app, videoX)
	require.Equal(t, http.StatusNotFound, rec.Code)
}

func getStatus(t *testing.T, app *web.App, videoID string) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, "/v/"+videoID+"/status", nil)
	req.Header.Set("HX-Request", "true")
	rec := httptest.NewRecorder()
	app.Router().ServeHTTP(rec, req)
	return rec
}

// setTranscriptStatus stamps videos.transcript_status directly (bypassing RLS via
// the super pool) so a test can drive the status endpoint into a given state.
func setTranscriptStatus(t *testing.T, p *pgxpool.Pool, videoID, status string) {
	t.Helper()
	_, err := p.Exec(context.Background(),
		`UPDATE videos SET transcript_status = $2 WHERE id = $1`, videoID, status)
	require.NoError(t, err)
}

// TestStatusRateLimitedShowsWaitingCard: a click that hit YouTube's rate limit
// must surface the honest "waiting, will retry" card that keeps polling — not a
// silent revert to the Summarize button.
func TestStatusRateLimitedShowsWaitingCard(t *testing.T) {
	app := newApp(t)
	p := rawPool(t)
	resetDB(t, p)
	seedVideo(t, p, videoX, "Throttled Title", "https://x", time.Time{})
	setTranscriptStatus(t, p, videoX, "rate_limited")

	html := body(t, getStatus(t, app, videoX))
	require.Contains(t, html, "Waiting on YouTube rate limits", "honest rate-limit copy")
	require.Contains(t, html, `hx-trigger="every 30s"`, "waiting card keeps polling so it self-resolves")
	require.NotContains(t, html, "Summarize this video", "must not revert to the Summarize button")
}

// TestStatusNoCaptionsTerminal: a no-captions outcome is terminal — an honest
// message, no poll, no button to click back into the same dead end.
func TestStatusNoCaptionsTerminal(t *testing.T) {
	app := newApp(t)
	p := rawPool(t)
	resetDB(t, p)
	seedVideo(t, p, videoX, "Silent Title", "https://x", time.Time{})
	setTranscriptStatus(t, p, videoX, "none")

	html := body(t, getStatus(t, app, videoX))
	require.Contains(t, html, "No captions available", "honest terminal copy")
	require.NotContains(t, html, "hx-trigger", "terminal card must stop polling")
}
