package gateway

import "testing"

func retryBucketCodeSet() map[string]struct{} {
	set := map[string]struct{}{}
	for _, bucket := range providerRetryCodeBuckets {
		if !bucket.retryable {
			continue
		}
		for _, code := range bucket.codes {
			set[normalizeProviderCode(code)] = struct{}{}
		}
	}
	return set
}

func allBucketCodeSet() map[string]struct{} {
	set := map[string]struct{}{}
	for _, bucket := range providerRetryCodeBuckets {
		for _, code := range bucket.codes {
			set[normalizeProviderCode(code)] = struct{}{}
		}
	}
	return set
}

func TestPayoutFailurePolicy_DecideProviderFailure(t *testing.T) {
	policy := defaultPayoutFailurePolicy()

	cases := []struct {
		name     string
		code     string
		action   payoutFailureAction
		strategy payoutRetryStrategy
	}{
		{
			name:     "immediate retry strategy code",
			code:     "10000",
			action:   payoutFailureActionRetry,
			strategy: payoutRetryStrategyImmediate,
		},
		{
			name:     "delayed retry strategy code",
			code:     "10101",
			action:   payoutFailureActionRetry,
			strategy: payoutRetryStrategyDelayed,
		},
		{
			name:     "status refresh retry strategy code",
			code:     "3061",
			action:   payoutFailureActionRetry,
			strategy: payoutRetryStrategyStatusRefresh,
		},
		{
			name:     "status refresh retry strategy payment details missing code",
			code:     "3062",
			action:   payoutFailureActionRetry,
			strategy: payoutRetryStrategyStatusRefresh,
		},
		{
			name:     "unknown provider code",
			code:     "99999",
			action:   payoutFailureActionFail,
			strategy: payoutRetryStrategyImmediate,
		},
		{
			name:     "empty provider code",
			code:     "",
			action:   payoutFailureActionFail,
			strategy: payoutRetryStrategyImmediate,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Helper()
			got := policy.decideProviderFailure(tc.code)
			if got.Action != tc.action {
				t.Fatalf("action mismatch: got=%v want=%v", got.Action, tc.action)
			}
			if got.Strategy != tc.strategy {
				t.Fatalf("strategy mismatch: got=%v want=%v", got.Strategy, tc.strategy)
			}
		})
	}
}

func TestPayoutFailurePolicy_DocumentRetryCoverage(t *testing.T) {
	policy := defaultPayoutFailurePolicy()

	// Parsed from GTX response-code tables (General, RCS, external card PS, external alternate PS, merchant system):
	// 32 immediate + 84 delayed + 6 status-refresh = 122 retryable codes.
	if got, want := len(policy.providerCodeStrategies), 122; got != want {
		t.Fatalf("retry catalog size mismatch: got=%d want=%d", got, want)
	}
	if got, want := len(policy.documentedProviderCodes), 395; got != want {
		t.Fatalf("documented code catalog size mismatch: got=%d want=%d", got, want)
	}

	cases := []struct {
		code     string
		strategy payoutRetryStrategy
	}{
		// Immediate retry examples.
		{code: "3025", strategy: payoutRetryStrategyImmediate},
		{code: "3198", strategy: payoutRetryStrategyImmediate},
		{code: "105012", strategy: payoutRetryStrategyImmediate},
		{code: "20603", strategy: payoutRetryStrategyImmediate},
		// Delayed retry examples, including previously missed high-range limits.
		{code: "3106", strategy: payoutRetryStrategyDelayed},
		{code: "3337", strategy: payoutRetryStrategyDelayed},
		{code: "3407", strategy: payoutRetryStrategyDelayed},
		{code: "3613", strategy: payoutRetryStrategyDelayed},
		{code: "201014", strategy: payoutRetryStrategyDelayed},
		// Status refresh examples.
		{code: "3061", strategy: payoutRetryStrategyStatusRefresh},
		{code: "3062", strategy: payoutRetryStrategyStatusRefresh},
		{code: "20802", strategy: payoutRetryStrategyStatusRefresh},
	}

	for _, tc := range cases {
		t.Run(tc.code, func(t *testing.T) {
			t.Helper()
			got := policy.decideProviderFailure(tc.code)
			if got.Action != payoutFailureActionRetry {
				t.Fatalf("action mismatch: got=%v want=%v", got.Action, payoutFailureActionRetry)
			}
			if got.Strategy != tc.strategy {
				t.Fatalf("strategy mismatch: got=%v want=%v", got.Strategy, tc.strategy)
			}
		})
	}
}

func TestPayoutFailurePolicy_DocumentedCodeCoverageByPolicy(t *testing.T) {
	policy := defaultPayoutFailurePolicy()
	retrySet := retryBucketCodeSet()

	if got, want := len(retrySet), len(policy.providerCodeStrategies); got != want {
		t.Fatalf("retry set size mismatch: got=%d want=%d", got, want)
	}

	documentedNonRetry := 0
	for _, code := range providerDocumentedCodes {
		code := normalizeProviderCode(code)
		decision := policy.decideProviderFailure(code)

		if _, isRetry := retrySet[code]; isRetry {
			if decision.Action != payoutFailureActionRetry {
				t.Fatalf("documented retry code %s unexpectedly classified as non-retry", code)
			}
			continue
		}

		documentedNonRetry++
		if decision.Action != payoutFailureActionFail {
			t.Fatalf("documented non-retry code %s unexpectedly classified as retry", code)
		}
		if decision.Reason != "provider_code_"+code+"_documented_non_retry" {
			t.Fatalf("documented non-retry code %s has unexpected reason: %q", code, decision.Reason)
		}
	}

	if got, want := len(retrySet)+documentedNonRetry, len(providerDocumentedCodes); got != want {
		t.Fatalf("coverage mismatch: retry(%d)+non_retry(%d) != documented(%d)", len(retrySet), documentedNonRetry, len(providerDocumentedCodes))
	}
}

func TestProviderRetryCodeBuckets_DoNotOverlapAndCoverDocumentedCodes(t *testing.T) {
	seen := map[string]int{}
	for bucketIdx, bucket := range providerRetryCodeBuckets {
		for _, rawCode := range bucket.codes {
			code := normalizeProviderCode(rawCode)
			if code == "" {
				t.Fatalf("empty code in bucket #%d", bucketIdx)
			}
			if prevIdx, ok := seen[code]; ok {
				t.Fatalf("overlap detected for code %s between bucket #%d and bucket #%d", code, prevIdx, bucketIdx)
			}
			seen[code] = bucketIdx
		}
	}

	allBucketCodes := allBucketCodeSet()
	documented := newCodeSet(providerDocumentedCodes)
	if got, want := len(allBucketCodes), len(documented); got != want {
		t.Fatalf("union size mismatch: buckets=%d documented=%d", got, want)
	}

	for code := range documented {
		if _, ok := allBucketCodes[code]; !ok {
			t.Fatalf("documented code %s is missing from providerRetryCodeBuckets union", code)
		}
	}
	for code := range allBucketCodes {
		if _, ok := documented[code]; !ok {
			t.Fatalf("bucket code %s is not present in documented code list", code)
		}
	}
}

func TestPayoutFailurePolicy_DecideProviderFailure_DocumentedNonRetryCode(t *testing.T) {
	policy := defaultPayoutFailurePolicy()

	got := policy.decideProviderFailure("3059")
	if got.Action != payoutFailureActionFail {
		t.Fatalf("action mismatch: got=%v want=%v", got.Action, payoutFailureActionFail)
	}
	if got.Strategy != payoutRetryStrategyImmediate {
		t.Fatalf("strategy mismatch: got=%v want=%v", got.Strategy, payoutRetryStrategyImmediate)
	}
	if got.Reason != "provider_code_3059_documented_non_retry" {
		t.Fatalf("reason mismatch: got=%q", got.Reason)
	}
}

func TestPayoutFailurePolicy_DecideProviderFailure_UnknownCode(t *testing.T) {
	policy := defaultPayoutFailurePolicy()

	got := policy.decideProviderFailure("99999")
	if got.Action != payoutFailureActionFail {
		t.Fatalf("action mismatch: got=%v want=%v", got.Action, payoutFailureActionFail)
	}
	if got.Strategy != payoutRetryStrategyImmediate {
		t.Fatalf("strategy mismatch: got=%v want=%v", got.Strategy, payoutRetryStrategyImmediate)
	}
	if got.Reason != "provider_code_99999_unknown" {
		t.Fatalf("reason mismatch: got=%q", got.Reason)
	}
}

func TestPayoutFailurePolicy_DecideTransportFailure(t *testing.T) {
	policy := defaultPayoutFailurePolicy()

	got := policy.decideTransportFailure()
	if got.Action != payoutFailureActionRetry {
		t.Fatalf("action mismatch: got=%v want=%v", got.Action, payoutFailureActionRetry)
	}
	if got.Strategy != payoutRetryStrategyImmediate {
		t.Fatalf("strategy mismatch: got=%v want=%v", got.Strategy, payoutRetryStrategyImmediate)
	}
}

func TestRetryDelayForAttempt_ByStrategy(t *testing.T) {
	cases := []struct {
		name      string
		attempt   uint32
		strategy  payoutRetryStrategy
		wantDelay int
	}{
		{
			name:      "immediate first attempt",
			attempt:   1,
			strategy:  payoutRetryStrategyImmediate,
			wantDelay: 5,
		},
		{
			name:      "delayed second attempt",
			attempt:   2,
			strategy:  payoutRetryStrategyDelayed,
			wantDelay: 120,
		},
		{
			name:      "status refresh third attempt",
			attempt:   3,
			strategy:  payoutRetryStrategyStatusRefresh,
			wantDelay: 40,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Helper()
			if got := retryDelayForAttempt(tc.attempt, tc.strategy); got != tc.wantDelay {
				t.Fatalf("delay mismatch: got=%d want=%d", got, tc.wantDelay)
			}
		})
	}
}

func TestPayoutFailureReason(t *testing.T) {
	if got, want := payoutFailureReason("10101", "Decline due to amount or frequency limit"), "10101: Decline due to amount or frequency limit"; got != want {
		t.Fatalf("failure reason mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutFailureReason("", "network error"), "network error"; got != want {
		t.Fatalf("failure reason mismatch: got=%q want=%q", got, want)
	}
	if got, want := payoutFailureReason("10101", ""), "10101"; got != want {
		t.Fatalf("failure reason mismatch: got=%q want=%q", got, want)
	}
}
