//go:build integration

package inttest

import (
	"context"
	"net/http"
	"strings"
	"testing"

	"github.com/google/uuid"

	"scrabble/backend/internal/payments"
)

// productByTitle finds a catalog product by its (unique in the test) title.
func productByTitle(t *testing.T, pay *payments.Service, title string) payments.AdminProduct {
	t.Helper()
	all, err := pay.AdminCatalog(context.Background(), true)
	if err != nil {
		t.Fatalf("admin catalog: %v", err)
	}
	for _, p := range all {
		if p.Title == title {
			return p
		}
	}
	t.Fatalf("product %q not found", title)
	return payments.AdminProduct{}
}

// TestConsoleCatalogEditor drives the catalog editor end to end: create is CSRF-guarded; a value and
// a pack are created and edited; an invalid active product is refused; archive hides it; a
// never-transacted product deletes; a transacted product is refused deletion (archive only).
func TestConsoleCatalogEditor(t *testing.T) {
	ctx := context.Background()
	srv, _, pay := bannerServer(t)
	h := srv.Handler()
	const origin = "http://admin.test"
	const catalog = "http://admin.test/_gm/catalog"

	// CSRF: a create without the origin header is refused.
	if code, _ := consoleDo(h, http.MethodPost, catalog, "title=x&hints=1&price_chip=10&active=true", ""); code != http.StatusForbidden {
		t.Fatalf("create without origin = %d, want 403", code)
	}

	// Create a value product: 5 hints for 50 chips, active.
	if code, body := consoleDo(h, http.MethodPost, catalog, "title=cat-hints-5&hints=5&price_chip=50&active=true", origin); code != http.StatusOK || !strings.Contains(body, "Created") {
		t.Fatalf("create value = %d, has 'Created' = %v", code, strings.Contains(body, "Created"))
	}
	val := productByTitle(t, pay, "cat-hints-5")
	if !val.Active || len(val.Atoms) != 1 || val.Atoms[0].Atom != "hints" || val.Atoms[0].Quantity != 5 {
		t.Fatalf("created value = %+v", val)
	}

	// An invalid active pack (chips, no money price) is refused.
	if code, body := consoleDo(h, http.MethodPost, catalog, "title=cat-bad&chips=100&active=true", origin); code != http.StatusOK || !strings.Contains(body, "Invalid product") {
		t.Fatalf("bad pack = %d, has 'Invalid product' = %v", code, strings.Contains(body, "Invalid product"))
	}
	if _, err := pay.AdminCatalog(ctx, true); err != nil {
		t.Fatalf("catalog: %v", err)
	}

	// Edit the value: raise to 8 hints.
	base := catalog + "/" + val.ID.String()
	if code, _ := consoleDo(h, http.MethodPost, base, "title=cat-hints-8&hints=8&price_chip=50&active=true", origin); code != http.StatusOK {
		t.Fatalf("edit = %d", code)
	}
	if got := productByTitle(t, pay, "cat-hints-8"); len(got.Atoms) != 1 || got.Atoms[0].Quantity != 8 {
		t.Fatalf("after edit atoms = %+v, want 8 hints", got.Atoms)
	}

	// Archive it → hidden from the storefront (the user Catalog filters active).
	if code, _ := consoleDo(h, http.MethodPost, base+"/archive", "active=false", origin); code != http.StatusOK {
		t.Fatalf("archive = %d", code)
	}
	if productByTitle(t, pay, "cat-hints-8").Active {
		t.Error("product still active after archive")
	}

	// Delete the never-transacted product.
	if code, body := consoleDo(h, http.MethodPost, base+"/delete", "", origin); code != http.StatusOK || !strings.Contains(body, "Deleted") {
		t.Fatalf("delete clean = %d, has 'Deleted' = %v", code, strings.Contains(body, "Deleted"))
	}

	// A transacted product cannot be deleted — only archived.
	if code, _ := consoleDo(h, http.MethodPost, catalog, "title=cat-pack&chips=100&price_rub=14900&active=true", origin); code != http.StatusOK {
		t.Fatal("create pack failed")
	}
	pack := productByTitle(t, pay, "cat-pack")
	if _, err := pay.CreateOrder(ctx, uuid.New(), payments.NewContext("direct", "web"), []payments.Source{payments.SourceDirect}, pack.ID, "robokassa"); err != nil {
		t.Fatalf("create order: %v", err)
	}
	if code, body := consoleDo(h, http.MethodPost, catalog+"/"+pack.ID.String()+"/delete", "", origin); code != http.StatusOK || !strings.Contains(body, "Cannot delete") {
		t.Fatalf("delete transacted = %d, has 'Cannot delete' = %v", code, strings.Contains(body, "Cannot delete"))
	}
}

// TestConsoleCatalogActiveToggle checks the catalog console lists only active products by default and
// includes the archived ones under ?all=1 (the active/all toggle).
func TestConsoleCatalogActiveToggle(t *testing.T) {
	srv, _, _ := bannerServer(t)
	h := srv.Handler()
	const origin = "http://admin.test"
	const catalog = "http://admin.test/_gm/catalog"

	// An active value and an archived one (created without the active flag).
	if code, _ := consoleDo(h, http.MethodPost, catalog, "title=toggle-active&hints=5&price_chip=50&active=true", origin); code != http.StatusOK {
		t.Fatal("create active failed")
	}
	if code, _ := consoleDo(h, http.MethodPost, catalog, "title=toggle-archived&hints=9&price_chip=90", origin); code != http.StatusOK {
		t.Fatal("create archived failed")
	}

	// Default view: active only.
	if _, body := consoleDo(h, http.MethodGet, catalog, "", ""); !strings.Contains(body, "toggle-active") || strings.Contains(body, "toggle-archived") {
		t.Errorf("default view: active listed=%v, archived listed=%v (want true/false)",
			strings.Contains(body, "toggle-active"), strings.Contains(body, "toggle-archived"))
	}

	// ?all=1: active and archived.
	if _, body := consoleDo(h, http.MethodGet, catalog+"?all=1", "", ""); !strings.Contains(body, "toggle-active") || !strings.Contains(body, "toggle-archived") {
		t.Errorf("all view: active listed=%v, archived listed=%v (want true/true)",
			strings.Contains(body, "toggle-active"), strings.Contains(body, "toggle-archived"))
	}
}

// TestConsoleRewardConfig checks the rewarded-ads config form on the catalog page: a POST updates the
// payout and caps, the page pre-fills the current values, and a negative value is refused.
func TestConsoleRewardConfig(t *testing.T) {
	srv, _, pay := bannerServer(t)
	h := srv.Handler()
	const origin = "http://admin.test"
	const reward = "http://admin.test/_gm/catalog/reward"

	// Set the payout and caps.
	if code, body := consoleDo(h, http.MethodPost, reward, "reward_payout=7&reward_daily=40&reward_hourly=9", origin); code != http.StatusOK || !strings.Contains(body, "Saved") {
		t.Fatalf("set reward = %d, has 'Saved' = %v", code, strings.Contains(body, "Saved"))
	}
	if payout, daily, hourly, err := pay.RewardConfig(context.Background()); err != nil || payout != 7 || daily != 40 || hourly != 9 {
		t.Fatalf("reward config = %d/%d/%d (err %v), want 7/40/9", payout, daily, hourly, err)
	}

	// The catalog page renders the form pre-filled with the current payout.
	if _, body := consoleDo(h, http.MethodGet, "http://admin.test/_gm/catalog", "", ""); !strings.Contains(body, "reward_payout") || !strings.Contains(body, `value="7"`) {
		t.Error("catalog page did not render the reward form with the current payout")
	}

	// A negative value is refused (the service validates non-negativity).
	if code, body := consoleDo(h, http.MethodPost, reward, "reward_payout=-1", origin); code != http.StatusOK || !strings.Contains(body, "non-negative") {
		t.Errorf("negative payout = %d, has 'non-negative' = %v", code, strings.Contains(body, "non-negative"))
	}
}
