package web

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

	"pete/internal/storage"
)

// newStore is a server with the adventure seam on, a signed-in buyer, and a
// board + catalog already pushed. Auth is built by hand for the same reason the
// casino tests do it: the OIDC handshake is a network call and none of what's
// under test is about it.
func newStore(t *testing.T) *Server {
	t.Helper()
	s, _ := newAdvServer(t, "tok")
	s.auth = &Authenticator{secret: []byte("test-secret-key-at-least-16")}

	now := time.Now().Unix()
	push := rosterPush{
		SnapshotAt:  now,
		Adventurers: []storage.RosterEntry{entry("tok-josie", "Josie", "expedition", "holymachina")},
		Tiers: []storage.MischiefTier{
			{Key: "grunt", Display: "Grunt", Fee: 40, SignedFee: 50},
			{Key: "boss", Display: "Boss", Fee: 1200, SignedFee: 1500},
		},
		Balances: []storage.MischiefBalance{{Username: "reala", Euro: 500}},
	}
	if w := postRoster(t, s, "tok", push); w.Code != 200 {
		t.Fatalf("seed roster = %d", w.Code)
	}
	return s
}

// jsonReq builds a bearer-authed (or unauthed, empty token) machine request.
func jsonReq(t *testing.T, method, path, token string, body any) *http.Request {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			t.Fatal(err)
		}
	}
	r := httptest.NewRequest(method, path, &buf)
	if token != "" {
		r.Header.Set("Authorization", "Bearer "+token)
	}
	return r
}

func placeOrder(t *testing.T, s *Server, username string, req mischiefOrderReq) *httptest.ResponseRecorder {
	t.Helper()
	r := as(t, s, username, "POST", "/api/mischief/order", req)
	w := httptest.NewRecorder()
	s.handleMischiefOrder(w, r)
	return w
}

// TestMischiefOrderHappyPath: a signed-in buyer names a mark on the board and a
// tier gogobee offers, and gets a pending order back.
func TestMischiefOrderHappyPath(t *testing.T) {
	s := newStore(t)

	w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt", Signed: false})
	if w.Code != 200 {
		t.Fatalf("order = %d body=%s", w.Code, w.Body.String())
	}
	var o storage.MischiefOrder
	if err := json.Unmarshal(w.Body.Bytes(), &o); err != nil {
		t.Fatal(err)
	}
	if o.Status != storage.MischiefPending || o.BuyerUsername != "reala" || o.TargetName != "Josie" {
		t.Fatalf("order = %+v", o)
	}
	if pending, _ := storage.PendingMischiefOrders(10); len(pending) != 1 {
		t.Fatalf("order didn't land in the pending set")
	}
}

func TestMischiefOrderRejections(t *testing.T) {
	s := newStore(t)

	if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "dragon"}); w.Code != 400 {
		t.Errorf("unknown tier = %d, want 400", w.Code)
	}
	if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "ghost", Tier: "grunt"}); w.Code != 404 {
		t.Errorf("off-board target = %d, want 404", w.Code)
	}
	if w := placeOrder(t, s, "reala", mischiefOrderReq{Tier: "grunt"}); w.Code != 400 {
		t.Errorf("empty target = %d, want 400", w.Code)
	}
	// Signed in, but no username the ledger can name (a pre-storefront session).
	if w := placeOrder(t, s, "", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 409 {
		t.Errorf("usernameless session = %d, want 409", w.Code)
	}
}

func TestMischiefOrderBurstGuard(t *testing.T) {
	s := newStore(t)
	for i := 0; i < mischiefBurstMax; i++ {
		if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 200 {
			t.Fatalf("order %d = %d, want 200", i, w.Code)
		}
	}
	if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 429 {
		t.Fatalf("over-cap order = %d, want 429", w.Code)
	}
}

func TestMischiefOrderRequiresAuth(t *testing.T) {
	s := newStore(t)
	r := httptest.NewRequest("POST", "/api/mischief/order", nil)
	w := httptest.NewRecorder()
	s.handleMischiefOrder(w, r)
	if w.Code != 401 {
		t.Fatalf("anonymous order = %d, want 401", w.Code)
	}
}

func TestMischiefCatalogCarriesBalance(t *testing.T) {
	s := newStore(t)
	r := as(t, s, "reala", "GET", "/api/mischief/catalog", nil)
	w := httptest.NewRecorder()
	s.handleMischiefCatalog(w, r)
	if w.Code != 200 {
		t.Fatalf("catalog = %d", w.Code)
	}
	var resp mischiefCatalogResp
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if len(resp.Tiers) != 2 || !resp.HasBalance || resp.Euro != 500 {
		t.Fatalf("catalog resp = %+v", resp)
	}
}

// ---- the bearer wire -----------------------------------------------------------

func TestMischiefPendingAndClaimBearer(t *testing.T) {
	s := newStore(t)
	if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "boss", Signed: true}); w.Code != 200 {
		t.Fatalf("seed order = %d", w.Code)
	}

	// Missing bearer is rejected.
	{
		w := httptest.NewRecorder()
		s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "", nil))
		if w.Code != 401 {
			t.Fatalf("no-bearer pending = %d, want 401", w.Code)
		}
	}

	// Poll returns the order.
	var pending []storage.MischiefOrder
	{
		w := httptest.NewRecorder()
		s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "tok", nil))
		if w.Code != 200 {
			t.Fatalf("pending = %d", w.Code)
		}
		if err := json.Unmarshal(w.Body.Bytes(), &pending); err != nil {
			t.Fatal(err)
		}
	}
	if len(pending) != 1 {
		t.Fatalf("pending count = %d, want 1", len(pending))
	}
	guid := pending[0].GUID

	// Claim it placed.
	{
		w := httptest.NewRecorder()
		s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok",
			mischiefVerdict{GUID: guid, Status: storage.MischiefPlaced, Detail: "out for delivery"}))
		if w.Code != 200 {
			t.Fatalf("claim = %d body=%s", w.Code, w.Body.String())
		}
	}
	if got, _ := storage.MischiefOrderByGUID(guid); got.Status != storage.MischiefPlaced {
		t.Fatalf("order status after claim = %q", got.Status)
	}

	// And it's gone from the pending set.
	{
		w := httptest.NewRecorder()
		s.handleMischiefPending(w, jsonReq(t, "GET", "/api/mischief/pending", "tok", nil))
		var again []storage.MischiefOrder
		_ = json.Unmarshal(w.Body.Bytes(), &again)
		if len(again) != 0 {
			t.Fatalf("placed order still polled: %d", len(again))
		}
	}
}

func TestMischiefClaimUnknownGUIDis400(t *testing.T) {
	s := newStore(t)
	w := httptest.NewRecorder()
	s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok",
		mischiefVerdict{GUID: "nope", Status: storage.MischiefPlaced}))
	if w.Code != 400 {
		t.Fatalf("unknown-guid claim = %d, want 400 (so gogobee parks it)", w.Code)
	}
}

func TestMischiefClaimBadVerdictIs400(t *testing.T) {
	s := newStore(t)
	if w := placeOrder(t, s, "reala", mischiefOrderReq{TargetToken: "tok-josie", Tier: "grunt"}); w.Code != 200 {
		t.Fatal("seed")
	}
	pending, _ := storage.PendingMischiefOrders(10)
	w := httptest.NewRecorder()
	s.handleMischiefClaim(w, jsonReq(t, "POST", "/api/mischief/claim", "tok",
		mischiefVerdict{GUID: pending[0].GUID, Status: "exploded"}))
	if w.Code != 400 {
		t.Fatalf("bad-verdict claim = %d, want 400", w.Code)
	}
}
