package api

import (
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

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

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

// authedRequest builds a request with a fake user in context, mirroring
// what the auth.RequireUser middleware injects in production.
// userIDByte fills every byte of the user's UUID, so two requests with
// different userIDByte values get distinct rate-limit slots.
func authedRequest(method, path string, userIDByte byte) *http.Request {
	user := dbq.User{
		ID: pgtype.UUID{
			Bytes: [16]byte{userIDByte, userIDByte, userIDByte, userIDByte,
				userIDByte, userIDByte, userIDByte, userIDByte,
				userIDByte, userIDByte, userIDByte, userIDByte,
				userIDByte, userIDByte, userIDByte, userIDByte},
			Valid: true,
		},
		Username: "tester",
	}
	r := httptest.NewRequest(method, path, nil)
	ctx := context.WithValue(r.Context(), auth.UserCtxKeyForTest(), user)
	return r.WithContext(ctx)
}

// withClientAPKDir points the handlers at a fresh temp dir for each
// test and restores the env var on cleanup. Returns the dir.
func withClientAPKDir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	prev, hadPrev := os.LookupEnv("MINSTREL_CLIENT_APK_DIR")
	t.Setenv("MINSTREL_CLIENT_APK_DIR", dir)
	t.Cleanup(func() {
		if hadPrev {
			t.Setenv("MINSTREL_CLIENT_APK_DIR", prev)
		}
		// t.Setenv auto-restores the prior empty/unset state at end of
		// test, so the !hadPrev branch needs no explicit Unsetenv.
	})
	return dir
}

func TestClientVersion_404WhenNoAPK(t *testing.T) {
	withClientAPKDir(t)
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	h.handleClientVersion(rr, httptest.NewRequest(http.MethodGet, "/api/client/version", nil))
	if rr.Code != http.StatusNotFound {
		t.Errorf("want 404, got %d", rr.Code)
	}
}

func TestClientVersion_404WhenAPKButNoVersion(t *testing.T) {
	dir := withClientAPKDir(t)
	if err := os.WriteFile(filepath.Join(dir, clientAPKFilename), []byte("fake apk"), 0o644); err != nil {
		t.Fatal(err)
	}
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	h.handleClientVersion(rr, httptest.NewRequest(http.MethodGet, "/api/client/version", nil))
	if rr.Code != http.StatusNotFound {
		t.Errorf("want 404, got %d", rr.Code)
	}
}

func TestClientVersion_200WithBothFiles(t *testing.T) {
	dir := withClientAPKDir(t)
	body := []byte("fake apk content")
	if err := os.WriteFile(filepath.Join(dir, clientAPKFilename), body, 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, clientVersionFile), []byte("v2026.05.10\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	h.handleClientVersion(rr, httptest.NewRequest(http.MethodGet, "/api/client/version", nil))
	if rr.Code != http.StatusOK {
		t.Fatalf("want 200, got %d (body: %s)", rr.Code, rr.Body.String())
	}
	var resp clientVersionResponse
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if resp.Version != "v2026.05.10" {
		t.Errorf("version: want trimmed v2026.05.10, got %q", resp.Version)
	}
	if resp.APKURL != "/api/client/apk" {
		t.Errorf("apk_url: want /api/client/apk, got %q", resp.APKURL)
	}
	if resp.SizeBytes != int64(len(body)) {
		t.Errorf("size_bytes: want %d, got %d", len(body), resp.SizeBytes)
	}
}

func TestClientAPK_401WhenUnauthenticated(t *testing.T) {
	withClientAPKDir(t)
	testResetClientAPKRateLimit()
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	// No user in context — middleware would block in prod; handler also
	// rejects defensively in case the route ever lands outside the
	// authed group by accident.
	h.handleClientAPK(rr, httptest.NewRequest(http.MethodGet, "/api/client/apk", nil))
	if rr.Code != http.StatusUnauthorized {
		t.Errorf("want 401, got %d", rr.Code)
	}
}

func TestClientAPK_404WhenMissing(t *testing.T) {
	withClientAPKDir(t)
	testResetClientAPKRateLimit()
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	h.handleClientAPK(rr, authedRequest(http.MethodGet, "/api/client/apk", 0x01))
	if rr.Code != http.StatusNotFound {
		t.Errorf("want 404, got %d", rr.Code)
	}
}

func TestClientAPK_StreamsWithCorrectContentType(t *testing.T) {
	dir := withClientAPKDir(t)
	testResetClientAPKRateLimit()
	body := []byte("PK\x03\x04 fake apk bytes")
	if err := os.WriteFile(filepath.Join(dir, clientAPKFilename), body, 0o644); err != nil {
		t.Fatal(err)
	}
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
	rr := httptest.NewRecorder()
	h.handleClientAPK(rr, authedRequest(http.MethodGet, "/api/client/apk", 0x02))
	if rr.Code != http.StatusOK {
		t.Fatalf("want 200, got %d", rr.Code)
	}
	if got := rr.Header().Get("Content-Type"); got != "application/vnd.android.package-archive" {
		t.Errorf("Content-Type: want application/vnd.android.package-archive, got %q", got)
	}
	if rr.Body.Len() != len(body) {
		t.Errorf("body length: want %d, got %d", len(body), rr.Body.Len())
	}
	if got := rr.Body.Bytes(); string(got) != string(body) {
		t.Errorf("body bytes mismatch")
	}
}

func TestClientAPK_RateLimit_429OnRapidSecondCall(t *testing.T) {
	dir := withClientAPKDir(t)
	testResetClientAPKRateLimit()
	if err := os.WriteFile(filepath.Join(dir, clientAPKFilename), []byte("apk"), 0o644); err != nil {
		t.Fatal(err)
	}
	h := &handlers{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}

	// First call from user 0x03 — succeeds.
	rr1 := httptest.NewRecorder()
	h.handleClientAPK(rr1, authedRequest(http.MethodGet, "/api/client/apk", 0x03))
	if rr1.Code != http.StatusOK {
		t.Fatalf("first call: want 200, got %d", rr1.Code)
	}

	// Immediate second call from same user — rate-limited.
	rr2 := httptest.NewRecorder()
	h.handleClientAPK(rr2, authedRequest(http.MethodGet, "/api/client/apk", 0x03))
	if rr2.Code != http.StatusTooManyRequests {
		t.Fatalf("second call: want 429, got %d", rr2.Code)
	}
	if got := rr2.Header().Get("Retry-After"); got == "" {
		t.Errorf("expected Retry-After header on 429 response")
	}

	// Different user — not rate-limited.
	rr3 := httptest.NewRecorder()
	h.handleClientAPK(rr3, authedRequest(http.MethodGet, "/api/client/apk", 0x04))
	if rr3.Code != http.StatusOK {
		t.Errorf("different user: want 200, got %d", rr3.Code)
	}
}
