package web

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

	"pete/internal/storage"
)

// The adventurer detail page. Two contracts under test: the public sheet reaches
// any visitor (stats + gear, never a handle), and the private self-view unlocks
// only for the signed-in owner of that exact page — anon and other users see the
// public page and nothing more.

func postDetail(t *testing.T, s *Server, token string, push detailPush) *httptest.ResponseRecorder {
	t.Helper()
	body, _ := json.Marshal(push)
	req := httptest.NewRequest("POST", "/api/ingest/detail", bytes.NewReader(body))
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	s.handleDetailIngest(w, req)
	return w
}

// publicDetail is the sheet gogobee hangs on a roster entry, marshalled the way
// the roster push carries it.
func publicDetail(t *testing.T) json.RawMessage {
	t.Helper()
	raw, err := json.Marshal(map[string]any{
		"hp_current":  30,
		"hp_max":      42,
		"temp_hp":     5,
		"armor_class": 17,
		"abilities":   [6]int{16, 14, 15, 10, 12, 8},
		"modifiers":   [6]int{3, 2, 2, 0, 1, -1},
		"gear": []map[string]any{
			{"slot": "weapon", "name": "Rusty Sword", "tier": 0, "condition": 100},
			{"slot": "armor", "name": "Padded Vest", "tier": 0, "condition": 100},
		},
		"supplies":     8,
		"threat_level": 2,
		"room":         "3 / 7",
	})
	if err != nil {
		t.Fatal(err)
	}
	return raw
}

// seedWho puts one adventurer on the board with a public detail sheet, and
// pushes a private self-detail set owned by `owner` for that same token.
func seedWho(t *testing.T, owner string) *Server {
	s, _ := newAdvServer(t, "tok")
	s.auth = &Authenticator{secret: []byte("test-secret-key-at-least-16")}
	now := time.Now().Unix()

	e := entry("tok-josie", "Josie", "expedition", "holymachina")
	e.Level = 14
	e.ClassRace = "human cleric"
	e.Detail = publicDetail(t)
	if w := postRoster(t, s, "tok", rosterPush{SnapshotAt: now, Adventurers: []storage.RosterEntry{e}}); w.Code != 200 {
		t.Fatalf("seed roster = %d", w.Code)
	}
	if w := postDetail(t, s, "tok", detailPush{SnapshotAt: now, Players: []storage.PlayerDetail{{
		Localpart: owner,
		Token:     "tok-josie",
		Inventory: []storage.ItemView{{Name: "Iron Ore", Type: "ore", Tier: 1, Value: 10}},
		Vault:     []storage.ItemView{{Name: "Jeweled Crown", Type: "treasure", Tier: 4, Value: 5000}},
		House:     storage.HouseView{Tier: 2, LoanBalance: 1500},
		Pets:      []storage.PetView{{Type: "cat", Name: "Mittens", Level: 3}},
	}}}); w.Code != 200 {
		t.Fatalf("seed detail = %d", w.Code)
	}
	return s
}

func getWho(t *testing.T, s *Server, token, asUser string) *httptest.ResponseRecorder {
	t.Helper()
	var req = httptest.NewRequest("GET", "/adventure/who/"+token, nil)
	req.SetPathValue("token", token)
	if asUser != "" {
		payload, _ := json.Marshal(SessionUser{Sub: "sub-1", Username: asUser, Exp: time.Now().Add(time.Hour).Unix()})
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: s.auth.sign(payload)})
	}
	w := httptest.NewRecorder()
	s.handleAdventureWho(w, req)
	return w
}

// TestWhoPublicSheet: any visitor sees the mark's stats and gear — driven
// through the real template, so a field slip 500s here rather than in prod.
func TestWhoPublicSheet(t *testing.T) {
	s := seedWho(t, "josie")

	w := getWho(t, s, "tok-josie", "") // anonymous
	if w.Code != 200 {
		t.Fatalf("GET who = %d, want 200", w.Code)
	}
	body := w.Body.String()
	for _, want := range []string{"Josie", "human cleric", "Rusty Sword", "Padded Vest"} {
		if !strings.Contains(body, want) {
			t.Errorf("public page missing %q", want)
		}
	}
	// The private goods must NOT be on an anonymous render.
	for _, leak := range []string{"Iron Ore", "Jeweled Crown", "Mittens"} {
		if strings.Contains(body, leak) {
			t.Errorf("anonymous page leaked private detail %q", leak)
		}
	}
}

// TestWhoSelfUnlock: the owner, signed in on their own page, sees the private
// inventory/vault/house/pets inline.
func TestWhoSelfUnlock(t *testing.T) {
	s := seedWho(t, "josie")

	w := getWho(t, s, "tok-josie", "josie")
	if w.Code != 200 {
		t.Fatalf("owner GET who = %d, want 200", w.Code)
	}
	body := w.Body.String()
	for _, want := range []string{"Iron Ore", "Jeweled Crown", "Mittens"} {
		if !strings.Contains(body, want) {
			t.Errorf("owner page missing private detail %q", want)
		}
	}
}

// TestWhoSelfUnlockCaseInsensitive: Authentik may hand back a mixed-case
// username; the localpart it maps to is lowercase. The owner must still unlock.
func TestWhoSelfUnlockCaseInsensitive(t *testing.T) {
	s := seedWho(t, "josie")

	w := getWho(t, s, "tok-josie", "Josie") // capital-J session
	if !strings.Contains(w.Body.String(), "Iron Ore") {
		t.Error("a mixed-case owner username failed to unlock their own self-view")
	}
}

// TestWhoOtherUserNoUnlock: a different signed-in player sees only the public
// sheet on someone else's page — the ownership join must not leak across users.
func TestWhoOtherUserNoUnlock(t *testing.T) {
	s := seedWho(t, "josie")

	w := getWho(t, s, "tok-josie", "quack") // signed in, but not the owner
	if w.Code != 200 {
		t.Fatalf("other-user GET who = %d, want 200", w.Code)
	}
	body := w.Body.String()
	if !strings.Contains(body, "Josie") {
		t.Error("public sheet missing for a signed-in non-owner")
	}
	for _, leak := range []string{"Iron Ore", "Jeweled Crown", "Mittens"} {
		if strings.Contains(body, leak) {
			t.Errorf("a non-owner unlocked private detail %q", leak)
		}
	}
}

// TestWhoOffBoardIs404: a token that isn't on the current board resolves to
// nothing — the same liveness gate the storefront uses, so a stale or guessed
// token never renders a page.
func TestWhoOffBoardIs404(t *testing.T) {
	s := seedWho(t, "josie")
	if w := getWho(t, s, "tok-ghost", ""); w.Code != 404 {
		t.Errorf("off-board token = %d, want 404", w.Code)
	}
}

// TestDetailIngestReplacesAndAuth: the detail push is bearer-gated and swaps the
// set whole, so a player dropped from a later push loses their self-view.
func TestDetailIngestReplacesAndAuth(t *testing.T) {
	s := seedWho(t, "josie")

	if w := postDetail(t, s, "wrong", detailPush{SnapshotAt: time.Now().Unix()}); w.Code != 401 {
		t.Errorf("bad bearer = %d, want 401", w.Code)
	}

	// A later push without Josie: her self-view must be gone, but the public
	// board (a separate keyspace) is untouched, so her page still renders public.
	if w := postDetail(t, s, "tok", detailPush{SnapshotAt: time.Now().Unix() + 60}); w.Code != 200 {
		t.Fatalf("empty detail push = %d, want 200", w.Code)
	}
	body := getWho(t, s, "tok-josie", "josie").Body.String()
	if strings.Contains(body, "Iron Ore") {
		t.Error("owner still unlocked after her detail was dropped from the push")
	}
	if !strings.Contains(body, "Josie") {
		t.Error("public page vanished when only the private detail was dropped")
	}
}
