package user_test

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

	backendserver "galaxy/backend/internal/server"
	"galaxy/backend/internal/server/middleware/basicauth"
	"galaxy/backend/internal/user"

	"github.com/google/uuid"
)

// TestUserSurfaceEndToEnd exercises the user-facing slice of the gin
// router with a real Postgres pool and the real handlers. It is the
// thinnest possible integration test that proves the wire layer wires
// the user.Service correctly — the contract test already validates the
// OpenAPI envelope on every endpoint, and the focused unit tests in
// user_test.go cover the business logic of Service.
func TestUserSurfaceEndToEnd(t *testing.T) {
	db := startPostgres(t)

	revoker := &recordingRevoker{}
	lobby := &recordingLobbyCascade{}
	notif := &recordingNotificationCascade{}
	geo := &recordingGeoCascade{}
	svc := user.NewService(user.Deps{

		Store: user.NewStore(db),
		Cache: user.NewCache(),
		Lobby: lobby,
		Notification: notif,
		Geo: geo,
		SessionRevoker: revoker,
		UserNameMaxRetries: 10,
		Now: time.Now,
	})

	uid, err := svc.EnsureByEmail(context.Background(), "nora@example.test", "en", "UTC", "")
	if err != nil {
		t.Fatalf("EnsureByEmail: %v", err)
	}

	const adminPassword = "user-e2e-test-secret"
	verifier := basicauth.NewStaticVerifier(adminPassword)
	handler, err := backendserver.NewRouter(backendserver.RouterDependencies{
		AdminVerifier: verifier,
		UserAccount: backendserver.NewUserAccountHandlers(svc, nil),
		AdminUsers: backendserver.NewAdminUsersHandlers(svc, nil),
		InternalUsers: backendserver.NewInternalUsersHandlers(svc, nil),
	})
	if err != nil {
		t.Fatalf("NewRouter: %v", err)
	}

	// 1. GET /api/v1/user/account → 200 with default `free` entitlement.
	resp := doRequest(t, handler, "GET", "/api/v1/user/account",
		header("X-User-ID", uid.String()), nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("user/account GET status = %d, want 200; body=%s", resp.Code, resp.Body.String())
	}
	body := decodeAccountResponse(t, resp)
	if body.Account.Entitlement.PlanCode != user.TierFree {
		t.Fatalf("entitlement.plan_code = %q, want %q", body.Account.Entitlement.PlanCode, user.TierFree)
	}

	// 2. PATCH profile → display_name updated.
	resp = doRequest(t, handler, "PATCH", "/api/v1/user/account/profile",
		header("X-User-ID", uid.String()),
		map[string]any{"display_name": "Nora"})
	if resp.Code != http.StatusOK {
		t.Fatalf("profile PATCH status = %d, want 200; body=%s", resp.Code, resp.Body.String())
	}
	body = decodeAccountResponse(t, resp)
	if body.Account.DisplayName != "Nora" {
		t.Fatalf("display_name = %q, want %q", body.Account.DisplayName, "Nora")
	}

	// 3. POST admin entitlement → tier flips to monthly.
	adminAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("operator:"+adminPassword))
	resp = doRequest(t, handler, "POST", "/api/v1/admin/users/"+uid.String()+"/entitlements",
		header("Authorization", adminAuth),
		map[string]any{
			"tier": "monthly",
			"source": "admin",
			"actor": map[string]any{"type": "admin", "id": "operator"},
		})
	if resp.Code != http.StatusOK {
		t.Fatalf("admin entitlement POST status = %d, want 200; body=%s", resp.Code, resp.Body.String())
	}
	body = decodeAccountResponse(t, resp)
	if body.Account.Entitlement.PlanCode != user.TierMonthly {
		t.Fatalf("entitlement.plan_code = %q, want %q", body.Account.Entitlement.PlanCode, user.TierMonthly)
	}
	if body.Account.Entitlement.EndsAt == nil {
		t.Fatalf("monthly entitlement returned ends_at = nil")
	}

	// 4. POST user soft-delete → 204.
	resp = doRequest(t, handler, "POST", "/api/v1/user/account/delete",
		header("X-User-ID", uid.String()), nil)
	if resp.Code != http.StatusNoContent {
		t.Fatalf("user delete POST status = %d, want 204; body=%s", resp.Code, resp.Body.String())
	}
	if revoker.calls != 1 {
		t.Fatalf("session revoker calls = %d, want 1", revoker.calls)
	}
	if lobby.deletedCalls != 1 {
		t.Fatalf("lobby.OnUserDeleted calls = %d, want 1", lobby.deletedCalls)
	}
	if notif.calls != 1 {
		t.Fatalf("notification.OnUserDeleted calls = %d, want 1", notif.calls)
	}
	if geo.calls != 1 {
		t.Fatalf("geo.OnUserDeleted calls = %d, want 1", geo.calls)
	}

	// 5. GET internal account-internal after soft-delete → 404.
	resp = doRequest(t, handler, "GET", "/api/v1/internal/users/"+uid.String()+"/account-internal", nil, nil)
	if resp.Code != http.StatusNotFound {
		t.Fatalf("internal account-internal GET after delete = %d, want 404; body=%s",
			resp.Code, resp.Body.String())
	}

	// 6. GET user/account on a fresh UUID → 404.
	stranger := uuid.New().String()
	resp = doRequest(t, handler, "GET", "/api/v1/user/account",
		header("X-User-ID", stranger), nil)
	if resp.Code != http.StatusNotFound {
		t.Fatalf("user/account GET stranger status = %d, want 404; body=%s", resp.Code, resp.Body.String())
	}
}

type accountResponseBody struct {
	Account struct {
		UserID string `json:"user_id"`
		Email string `json:"email"`
		UserName string `json:"user_name"`
		DisplayName string `json:"display_name"`
		Entitlement struct {
			PlanCode string `json:"plan_code"`
			IsPaid bool `json:"is_paid"`
			MaxRegisteredRaceNames int32 `json:"max_registered_race_names"`
			EndsAt *string `json:"ends_at"`
		} `json:"entitlement"`
	} `json:"account"`
}

type headerKV struct{ key, value string }

func header(key, value string) headerKV { return headerKV{key: key, value: value} }

func doRequest(t *testing.T, handler http.Handler, method, path string, hdr any, body map[string]any) *httptest.ResponseRecorder {
	t.Helper()
	var rdr *bytes.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("marshal body: %v", err)
		}
		rdr = bytes.NewReader(raw)
	} else {
		rdr = bytes.NewReader(nil)
	}
	req, err := http.NewRequest(method, "http://backend.internal"+path, rdr)
	if err != nil {
		t.Fatalf("NewRequest: %v", err)
	}
	req.Header.Set("Accept", "application/json")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if hdr != nil {
		switch h := hdr.(type) {
		case headerKV:
			req.Header.Set(h.key, h.value)
		case []headerKV:
			for _, kv := range h {
				req.Header.Set(kv.key, kv.value)
			}
		}
	}
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)
	return rec
}

func decodeAccountResponse(t *testing.T, rec *httptest.ResponseRecorder) accountResponseBody {
	t.Helper()
	var out accountResponseBody
	if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
		t.Fatalf("unmarshal AccountResponse: %v; body=%s", err, rec.Body.String())
	}
	return out
}
