//go:build integration

package inttest

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

	"github.com/google/uuid"
	"go.uber.org/zap/zaptest"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/server"
)

// blockStatusBody mirrors the backend's /block-status JSON for the test.
type blockStatusBody struct {
	Blocked   bool   `json:"blocked"`
	Permanent bool   `json:"permanent"`
	Until     string `json:"until"`
	Reason    string `json:"reason"`
}

// TestSuspensionGate drives the gate end-to-end through the assembled HTTP server: a blocked
// account is refused on a normal user route with 403 + account_blocked, the block-status probe
// stays reachable and resolves the reason to the account's language, and an unblock restores
// access.
func TestSuspensionGate(t *testing.T) {
	ctx := context.Background()
	accounts := account.NewStore(testDB)
	srv := server.New(":0", server.Deps{
		Logger:   zaptest.NewLogger(t),
		DB:       testDB,
		Accounts: accounts,
	})

	acc, _, err := accounts.ProvisionTelegram(ctx, "tg-"+uuid.NewString(), "ru", "", "Blocked", "")
	if err != nil {
		t.Fatalf("provision: %v", err)
	}
	id := acc.ID

	// Not blocked: a normal route passes and block-status reports false.
	if rec := userGet(t, srv, "/api/v1/user/profile", id); rec.Code != http.StatusOK {
		t.Fatalf("profile before block = %d, want 200", rec.Code)
	}
	if bs := blockStatus(t, srv, id); bs.Blocked {
		t.Fatal("fresh account should not be blocked")
	}

	// Block permanently with a reason.
	if _, err := accounts.Suspend(ctx, id, nil, "Spam", "Спам", nil); err != nil {
		t.Fatalf("suspend: %v", err)
	}

	// A normal route is now refused with the stable code.
	rec := userGet(t, srv, "/api/v1/user/profile", id)
	if rec.Code != http.StatusForbidden {
		t.Fatalf("profile while blocked = %d, want 403", rec.Code)
	}
	if code := errorCode(t, rec); code != "account_blocked" {
		t.Fatalf("error code = %q, want account_blocked", code)
	}

	// The block-status probe stays reachable and resolves the reason to the account's language.
	bs := blockStatus(t, srv, id)
	if !bs.Blocked || !bs.Permanent {
		t.Fatalf("block-status = %+v, want blocked+permanent", bs)
	}
	if bs.Reason != "Спам" {
		t.Errorf("reason = %q, want the Russian snapshot Спам", bs.Reason)
	}
	if bs.Until != "" {
		t.Errorf("until = %q, want empty for a permanent block", bs.Until)
	}

	// Unblock restores access.
	if err := accounts.LiftSuspension(ctx, id); err != nil {
		t.Fatalf("lift: %v", err)
	}
	if rec := userGet(t, srv, "/api/v1/user/profile", id); rec.Code != http.StatusOK {
		t.Fatalf("profile after unblock = %d, want 200", rec.Code)
	}
}

// TestSuspensionGateTemporaryUntil checks a temporary block reports its expiry through
// block-status as a future RFC3339 instant and not permanent.
func TestSuspensionGateTemporaryUntil(t *testing.T) {
	ctx := context.Background()
	accounts := account.NewStore(testDB)
	srv := server.New(":0", server.Deps{Logger: zaptest.NewLogger(t), DB: testDB, Accounts: accounts})
	id := provisionAccount(t)

	until := time.Now().Add(24 * time.Hour)
	if _, err := accounts.Suspend(ctx, id, &until, "", "", nil); err != nil {
		t.Fatalf("suspend: %v", err)
	}
	bs := blockStatus(t, srv, id)
	if !bs.Blocked || bs.Permanent {
		t.Fatalf("block-status = %+v, want blocked, not permanent", bs)
	}
	parsed, err := time.Parse(time.RFC3339, bs.Until)
	if err != nil {
		t.Fatalf("until %q is not RFC3339: %v", bs.Until, err)
	}
	if !parsed.After(time.Now()) {
		t.Errorf("until = %v, want a future instant", parsed)
	}
}

// userGet issues an authenticated GET (X-User-ID) against the assembled server.
func userGet(t *testing.T, srv *server.Server, path string, id uuid.UUID) *httptest.ResponseRecorder {
	t.Helper()
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, path, nil)
	req.Header.Set("X-User-ID", id.String())
	srv.Handler().ServeHTTP(rec, req)
	return rec
}

// blockStatus fetches and decodes the /block-status payload, asserting a 200.
func blockStatus(t *testing.T, srv *server.Server, id uuid.UUID) blockStatusBody {
	t.Helper()
	rec := userGet(t, srv, "/api/v1/user/block-status", id)
	if rec.Code != http.StatusOK {
		t.Fatalf("block-status status = %d, want 200", rec.Code)
	}
	var b blockStatusBody
	if err := json.Unmarshal(rec.Body.Bytes(), &b); err != nil {
		t.Fatalf("decode block-status: %v", err)
	}
	return b
}

// errorCode extracts the stable code from the backend error envelope.
func errorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
	t.Helper()
	var e struct {
		Error struct {
			Code string `json:"code"`
		} `json:"error"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &e); err != nil {
		t.Fatalf("decode error envelope: %v", err)
	}
	return e.Error.Code
}
