//go:build integration

package inttest

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/account"
)

// TestSuspensionRoundTrip covers a permanent block: a fresh account is not blocked, Suspend with
// a reason makes CurrentSuspension report it (permanent, with the en/ru snapshot resolved per
// language), and LiftSuspension reverses it.
func TestSuspensionRoundTrip(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	id := provisionAccount(t)

	if _, ok, err := store.CurrentSuspension(ctx, id); err != nil || ok {
		t.Fatalf("fresh CurrentSuspension = (ok %v, err %v), want (false, nil)", ok, err)
	}

	if _, err := store.Suspend(ctx, id, nil, "Spam", "Спам", nil); err != nil {
		t.Fatalf("suspend: %v", err)
	}

	susp, ok, err := store.CurrentSuspension(ctx, id)
	if err != nil || !ok {
		t.Fatalf("CurrentSuspension after suspend = (ok %v, err %v), want (true, nil)", ok, err)
	}
	if !susp.Permanent() {
		t.Errorf("Permanent() = false, want true for a nil-until block")
	}
	if got := susp.LocalizedReason("en"); got != "Spam" {
		t.Errorf("LocalizedReason(en) = %q, want Spam", got)
	}
	if got := susp.LocalizedReason("ru"); got != "Спам" {
		t.Errorf("LocalizedReason(ru) = %q, want Спам", got)
	}
	if got := susp.LocalizedReason("fr"); got != "Spam" {
		t.Errorf("LocalizedReason(fr) = %q, want the English fallback Spam", got)
	}

	if err := store.LiftSuspension(ctx, id); err != nil {
		t.Fatalf("lift: %v", err)
	}
	if _, ok, err := store.CurrentSuspension(ctx, id); err != nil || ok {
		t.Fatalf("CurrentSuspension after lift = (ok %v, err %v), want (false, nil)", ok, err)
	}
}

// TestTemporarySuspensionExpiry checks the until boundary: a future expiry is in force (and not
// permanent), while a past expiry is already not in force.
func TestTemporarySuspensionExpiry(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)

	future := provisionAccount(t)
	until := time.Now().Add(time.Hour)
	if _, err := store.Suspend(ctx, future, &until, "", "", nil); err != nil {
		t.Fatalf("suspend future: %v", err)
	}
	susp, ok, err := store.CurrentSuspension(ctx, future)
	if err != nil || !ok {
		t.Fatalf("future CurrentSuspension = (ok %v, err %v), want (true, nil)", ok, err)
	}
	if susp.Permanent() {
		t.Error("Permanent() = true, want false for a dated block")
	}
	if susp.BlockedUntil == nil || !susp.BlockedUntil.After(time.Now()) {
		t.Errorf("BlockedUntil = %v, want a future instant", susp.BlockedUntil)
	}

	past := provisionAccount(t)
	expired := time.Now().Add(-time.Hour)
	if _, err := store.Suspend(ctx, past, &expired, "", "", nil); err != nil {
		t.Fatalf("suspend past: %v", err)
	}
	if _, ok, err := store.CurrentSuspension(ctx, past); err != nil || ok {
		t.Fatalf("expired CurrentSuspension = (ok %v, err %v), want (false, nil)", ok, err)
	}
}

// TestSuspensionStrongestWins checks that with overlapping blocks CurrentSuspension returns the
// strongest — a permanent block outranks any dated one — and that LiftSuspension clears them all.
func TestSuspensionStrongestWins(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	id := provisionAccount(t)

	soon := time.Now().Add(time.Hour)
	if _, err := store.Suspend(ctx, id, &soon, "", "", nil); err != nil {
		t.Fatalf("suspend dated: %v", err)
	}
	if _, err := store.Suspend(ctx, id, nil, "", "", nil); err != nil {
		t.Fatalf("suspend permanent: %v", err)
	}
	later := time.Now().Add(2 * time.Hour)
	if _, err := store.Suspend(ctx, id, &later, "", "", nil); err != nil {
		t.Fatalf("suspend dated-later: %v", err)
	}

	susp, ok, err := store.CurrentSuspension(ctx, id)
	if err != nil || !ok {
		t.Fatalf("CurrentSuspension = (ok %v, err %v), want (true, nil)", ok, err)
	}
	if !susp.Permanent() {
		t.Error("strongest block should be the permanent one")
	}

	if err := store.LiftSuspension(ctx, id); err != nil {
		t.Fatalf("lift: %v", err)
	}
	if _, ok, err := store.CurrentSuspension(ctx, id); err != nil || ok {
		t.Fatalf("CurrentSuspension after lift = (ok %v, err %v), want (false, nil)", ok, err)
	}
}

// TestReasonsCRUD covers the operator-editable picklist: create, list, get, update, delete, and
// the ErrNotFound paths.
func TestReasonsCRUD(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)

	created, err := store.CreateReason(ctx, "Cheating", "Читерство")
	if err != nil {
		t.Fatalf("create reason: %v", err)
	}
	if created.ID == uuid.Nil || created.TextEn != "Cheating" || created.TextRu != "Читерство" {
		t.Fatalf("created reason = %+v, want populated en/ru and id", created)
	}

	got, err := store.GetReason(ctx, created.ID)
	if err != nil {
		t.Fatalf("get reason: %v", err)
	}
	if got.TextEn != "Cheating" {
		t.Errorf("get TextEn = %q, want Cheating", got.TextEn)
	}

	list, err := store.ListReasons(ctx)
	if err != nil {
		t.Fatalf("list reasons: %v", err)
	}
	if !containsReason(list, created.ID) {
		t.Error("created reason missing from ListReasons")
	}

	updated, err := store.UpdateReason(ctx, created.ID, "Abuse", "Оскорбления")
	if err != nil {
		t.Fatalf("update reason: %v", err)
	}
	if updated.TextEn != "Abuse" || updated.TextRu != "Оскорбления" {
		t.Errorf("updated reason = %+v, want Abuse/Оскорбления", updated)
	}

	if err := store.DeleteReason(ctx, created.ID); err != nil {
		t.Fatalf("delete reason: %v", err)
	}
	if _, err := store.GetReason(ctx, created.ID); !errors.Is(err, account.ErrNotFound) {
		t.Errorf("get deleted reason = %v, want ErrNotFound", err)
	}
	if _, err := store.UpdateReason(ctx, uuid.New(), "x", "y"); !errors.Is(err, account.ErrNotFound) {
		t.Errorf("update missing reason = %v, want ErrNotFound", err)
	}
}

// TestSuspensionReasonSnapshotSurvivesDelete checks that a block keeps the reason text it was
// stamped with even after the picklist entry is deleted (the FK nulls reason_id, the snapshot
// stays).
func TestSuspensionReasonSnapshotSurvivesDelete(t *testing.T) {
	ctx := context.Background()
	store := account.NewStore(testDB)
	id := provisionAccount(t)

	reason, err := store.CreateReason(ctx, "Toxicity", "Токсичность")
	if err != nil {
		t.Fatalf("create reason: %v", err)
	}
	if _, err := store.Suspend(ctx, id, nil, reason.TextEn, reason.TextRu, &reason.ID); err != nil {
		t.Fatalf("suspend with reason: %v", err)
	}
	if err := store.DeleteReason(ctx, reason.ID); err != nil {
		t.Fatalf("delete reason: %v", err)
	}

	susp, ok, err := store.CurrentSuspension(ctx, id)
	if err != nil || !ok {
		t.Fatalf("CurrentSuspension = (ok %v, err %v), want (true, nil)", ok, err)
	}
	if susp.LocalizedReason("en") != "Toxicity" || susp.LocalizedReason("ru") != "Токсичность" {
		t.Errorf("reason snapshot lost after delete: en=%q ru=%q", susp.LocalizedReason("en"), susp.LocalizedReason("ru"))
	}
}

// containsReason reports whether list holds a reason with the given id.
func containsReason(list []account.Reason, id uuid.UUID) bool {
	for _, r := range list {
		if r.ID == id {
			return true
		}
	}
	return false
}
