package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// nonExistentTrackUUID is used by tests that exercise paths which don't
// require the track to actually exist (auth/UUID-shape rejection).
const nonExistentTrackUUID = "11111111-1111-1111-1111-111111111111"

func TestCastStreamToken_HappyPath(t *testing.T) {
	h, pool := testHandlers(t)
	user := seedUser(t, pool, "alice", "hunter2", false)
	artist := seedArtist(t, pool, "Artist")
	album := seedAlbum(t, pool, artist.ID, "Album", 0)
	track := seedTrack(t, pool, album.ID, artist.ID, "Song", 1, 180_000)
	trackID := uuidToString(track.ID)
	h.streamSecret = []byte("cast-token-test-secret")

	body, err := json.Marshal(castTokenRequest{
		TrackID:    trackID,
		ExpSeconds: 3600,
	})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	req := httptest.NewRequest(http.MethodPost, "/api/cast/stream-token", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, user)
	w := httptest.NewRecorder()

	h.handleCastStreamToken(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
	}
	var resp castTokenResponse
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Token == "" || resp.Exp == 0 || resp.URL == "" {
		t.Fatalf("empty fields in response: %+v", resp)
	}
	if !strings.Contains(resp.URL, "token="+resp.Token) {
		t.Fatalf("URL missing token query: %s", resp.URL)
	}
	if !strings.Contains(resp.URL, "/api/tracks/"+trackID+"/stream") {
		t.Fatalf("URL missing stream path: %s", resp.URL)
	}
	// Stream URL must carry a file extension so Sonos's URL probe can
	// identify the audio format (see task #610). Track seeded above is
	// .flac via seedTrack's default file_format.
	if !strings.Contains(resp.URL, "/stream.flac?") {
		t.Fatalf("URL missing file-extension segment: %s", resp.URL)
	}
	if !VerifyStreamToken(h.streamSecret, trackID, resp.Exp, resp.Token) {
		t.Fatal("returned token does not verify")
	}
}

func TestCastStreamToken_RejectsBadUUID(t *testing.T) {
	h, pool := testHandlers(t)
	user := seedUser(t, pool, "alice", "hunter2", false)
	h.streamSecret = []byte("cast-token-test-secret")

	body, err := json.Marshal(castTokenRequest{TrackID: "not-a-uuid"})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	req := httptest.NewRequest(http.MethodPost, "/api/cast/stream-token", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, user)
	w := httptest.NewRecorder()

	h.handleCastStreamToken(w, req)

	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400; body=%s", w.Code, w.Body.String())
	}
}

func TestCastStreamToken_RejectsUnauthenticated(t *testing.T) {
	h, _ := testHandlers(t)
	h.streamSecret = []byte("cast-token-test-secret")

	body, err := json.Marshal(castTokenRequest{TrackID: nonExistentTrackUUID})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	req := httptest.NewRequest(http.MethodPost, "/api/cast/stream-token", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	// NO withUser — handler must reject via requireUser.
	w := httptest.NewRecorder()

	h.handleCastStreamToken(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
	}
}

func TestCastStreamToken_ClampsExpSeconds(t *testing.T) {
	h, pool := testHandlers(t)
	user := seedUser(t, pool, "alice", "hunter2", false)
	artist := seedArtist(t, pool, "Artist")
	album := seedAlbum(t, pool, artist.ID, "Album", 0)
	track := seedTrack(t, pool, album.ID, artist.ID, "Song", 1, 180_000)
	trackID := uuidToString(track.ID)
	h.streamSecret = []byte("cast-token-test-secret")

	// Request 1 second (below min 60), expect clamp to 60s.
	body, err := json.Marshal(castTokenRequest{
		TrackID:    trackID,
		ExpSeconds: 1,
	})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	before := time.Now().Unix()
	req := httptest.NewRequest(http.MethodPost, "/api/cast/stream-token", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = withUser(req, user)
	w := httptest.NewRecorder()

	h.handleCastStreamToken(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
	}
	var resp castTokenResponse
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	// Allow a small jitter window around the clamp target (60s).
	delta := resp.Exp - before
	if delta < 55 || delta > 70 {
		t.Fatalf("expected ~60s expiry after clamp, got delta=%ds", delta)
	}
}

func TestClampExpSeconds(t *testing.T) {
	cases := []struct {
		name string
		in   int
		want int
	}{
		{"zero defaults to 6h", 0, castTokenDefaultExp},
		{"negative defaults to 6h", -1, castTokenDefaultExp},
		{"below min clamps up", 30, castTokenMinExpSeconds},
		{"exact min passes through", castTokenMinExpSeconds, castTokenMinExpSeconds},
		{"mid-range passes through", 3600, 3600},
		{"exact max passes through", castTokenMaxExpSeconds, castTokenMaxExpSeconds},
		{"above max clamps down", castTokenMaxExpSeconds + 1, castTokenMaxExpSeconds},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := clampExpSeconds(tc.in); got != tc.want {
				t.Fatalf("clampExpSeconds(%d) = %d, want %d", tc.in, got, tc.want)
			}
		})
	}
}
