package integration

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/niels/banking-app/backend/internal/accounts"
	"github.com/niels/banking-app/backend/internal/cards"
	"github.com/niels/banking-app/backend/internal/kyc"
	"github.com/niels/banking-app/backend/internal/ledger"
	"github.com/niels/banking-app/backend/internal/transfers"
)

func TestWebhookReplayAndProviderDecisionIdempotency(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	db := openTestDatabase(t, ctx)
	resetDatabase(t, ctx, db)
	runMigrations(t, ctx, db)

	cardRepo := cards.NewRepository(db)
	inserted, err := cardRepo.RecordWebhookEvent(ctx, "evt-card-001", "authorization.requested", []byte(`{"pan":"4111111111111111","cvv":"123"}`))
	if err != nil {
		t.Fatalf("record first card webhook event: %v", err)
	}
	if !inserted {
		t.Fatal("expected first card webhook event to be inserted")
	}
	inserted, err = cardRepo.RecordWebhookEvent(ctx, "evt-card-001", "authorization.requested", []byte(`{"pan":"4111111111111111","cvv":"123"}`))
	if err != nil {
		t.Fatalf("record replayed card webhook event: %v", err)
	}
	if inserted {
		t.Fatal("expected replayed card webhook event to be ignored")
	}
	assertSingleRow(t, ctx, db, `SELECT COUNT(*) FROM card_issuer_webhook_events WHERE external_event_id = 'evt-card-001'`, "card webhook replay")
	assertNoRawCardData(t, ctx, db)

	userID := createUser(t, ctx, db, "kyc-webhook@example.test", "KYC Webhook")
	kycRepo := kyc.NewRepository(db)
	profile, err := kycRepo.UpsertProfile(ctx, kyc.UpsertProfileParams{
		UserID:       userID,
		LegalName:    "KYC Webhook",
		DateOfBirth:  "1990-01-01",
		Country:      "NL",
		AddressLine1: "Teststraat 1",
		City:         "Amsterdam",
		PostalCode:   "1000AA",
	})
	if err != nil {
		t.Fatalf("create KYC profile: %v", err)
	}
	raw, _ := json.Marshal(map[string]string{"event": "evt-kyc-001"})
	firstDecision, err := kycRepo.RecordProviderDecision(ctx, kyc.ProviderDecisionParams{
		UserID:                 userID,
		KYCProfileID:           profile.ID,
		Provider:               "local_identity",
		ExternalVerificationID: "verif-001",
		ProviderEventID:        "evt-kyc-001",
		Decision:               "verified",
		Reason:                 "provider_webhook",
		RawPayload:             raw,
	})
	if err != nil {
		t.Fatalf("record first KYC provider decision: %v", err)
	}
	secondDecision, err := kycRepo.RecordProviderDecision(ctx, kyc.ProviderDecisionParams{
		UserID:                 userID,
		KYCProfileID:           profile.ID,
		Provider:               "local_identity",
		ExternalVerificationID: "verif-001",
		ProviderEventID:        "evt-kyc-001",
		Decision:               "verified",
		Reason:                 "provider_webhook_replay",
		RawPayload:             raw,
	})
	if err != nil {
		t.Fatalf("record replayed KYC provider decision: %v", err)
	}
	if firstDecision.ID != secondDecision.ID {
		t.Fatalf("expected replayed KYC decision to update same row, got %s and %s", firstDecision.ID, secondDecision.ID)
	}
	assertSingleRow(t, ctx, db, `SELECT COUNT(*) FROM kyc_provider_decisions WHERE provider_event_id = 'evt-kyc-001'`, "KYC provider decision replay")
}

func TestPaymentIdempotencyKeyReturnsSameTransfer(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	db := openTestDatabase(t, ctx)
	resetDatabase(t, ctx, db)
	runMigrations(t, ctx, db)

	accountRepo := accounts.NewRepository(db)
	ledgerRepo := ledger.NewRepository(db)
	transferRepo := transfers.NewRepository(db, ledgerRepo)

	sourceUserID := createUser(t, ctx, db, "idempotent-source@example.test", "Idempotent Source")
	destinationUserID := createUser(t, ctx, db, "idempotent-destination@example.test", "Idempotent Destination")
	sourceAccount, err := accountRepo.Create(ctx, accounts.CreateParams{UserID: sourceUserID, AccountNumber: "2000000001", Currency: "EUR"})
	if err != nil {
		t.Fatalf("create source account: %v", err)
	}
	destinationAccount, err := accountRepo.Create(ctx, accounts.CreateParams{UserID: destinationUserID, AccountNumber: "2000000002", Currency: "EUR"})
	if err != nil {
		t.Fatalf("create destination account: %v", err)
	}
	if _, err := db.Exec(ctx, `UPDATE accounts SET balance_cents = 10000 WHERE id = $1`, sourceAccount.ID); err != nil {
		t.Fatalf("fund source account row: %v", err)
	}
	postInitialFunding(t, ctx, db, ledgerRepo, sourceUserID, sourceAccount.ID, "EUR", 10000)

	params := transfers.CreateParams{
		UserID:         sourceUserID,
		FromAccountID:  sourceAccount.ID,
		ToAccountID:    destinationAccount.ID,
		AmountCents:    1250,
		Description:    "idempotency quality gate",
		IdempotencyKey: "transfer-idempotency-quality-gate",
	}
	first, err := transferRepo.Create(ctx, params)
	if err != nil {
		t.Fatalf("create first transfer: %v", err)
	}
	second, err := transferRepo.Create(ctx, params)
	if err != nil {
		t.Fatalf("create replayed transfer: %v", err)
	}
	if first.ID != second.ID {
		t.Fatalf("expected same transfer for replayed idempotency key, got %s and %s", first.ID, second.ID)
	}
	assertSingleRow(t, ctx, db, `SELECT COUNT(*) FROM transfers WHERE idempotency_key = 'transfer-idempotency-quality-gate'`, "transfer idempotency")
	assertLedgerJournalEntriesBalanced(t, ctx, db)
}

func assertSingleRow(t *testing.T, ctx context.Context, db *pgxpool.Pool, query, label string) {
	t.Helper()
	var count int
	if err := db.QueryRow(ctx, query).Scan(&count); err != nil {
		t.Fatalf("%s count query: %v", label, err)
	}
	if count != 1 {
		t.Fatalf("%s count = %d, want 1", label, count)
	}
}

func assertNoRawCardData(t *testing.T, ctx context.Context, db *pgxpool.Pool) {
	t.Helper()
	var payload string
	if err := db.QueryRow(ctx, `SELECT payload FROM card_issuer_webhook_events WHERE external_event_id = 'evt-card-001'`).Scan(&payload); err != nil {
		t.Fatalf("read stored card webhook payload: %v", err)
	}
	for _, forbidden := range []string{"4111111111111111", `"123"`} {
		if strings.Contains(payload, forbidden) {
			t.Fatalf("stored card webhook payload leaked %s: %s", forbidden, payload)
		}
	}
}
