package de.gecheckt.pdf.umbenenner.application.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.out.DocumentErrorClassification;
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.ImmediateRetryDecision;
import de.gecheckt.pdf.umbenenner.application.port.out.RetryDecision;

/**
 * Tests for {@link DefaultRetryDecisionEvaluator}.
 * <p>
 * Verifies the binding retry policy rules for deterministic content errors,
 * transient technical errors, target copy failures, and the within-run
 * immediate retry mechanism.
 */
class DefaultRetryDecisionEvaluatorTest {

    private static final String FAILURE_CLASS = "SOME_FAILURE";
    private static final String FAILURE_MESSAGE = "Something went wrong";

    private DefaultRetryDecisionEvaluator evaluator;

    @BeforeEach
    void setUp() {
        evaluator = new DefaultRetryDecisionEvaluator();
    }

    // -------------------------------------------------------------------------
    // Deterministic content error rules
    // -------------------------------------------------------------------------

    @Test
    void evaluate_firstContentError_returnsContentErrorRetryable() {
        FailureCounters counters = new FailureCounters(0, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.ContentErrorRetryable.class, decision);
        RetryDecision.ContentErrorRetryable retryable = (RetryDecision.ContentErrorRetryable) decision;
        assertEquals(FAILURE_CLASS, retryable.failureClass());
        assertEquals(FAILURE_MESSAGE, retryable.failureMessage());
    }

    @Test
    void evaluate_secondContentError_returnsContentErrorFinal() {
        FailureCounters counters = new FailureCounters(1, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.ContentErrorFinal.class, decision);
        RetryDecision.ContentErrorFinal finalDecision = (RetryDecision.ContentErrorFinal) decision;
        assertEquals(FAILURE_CLASS, finalDecision.failureClass());
        assertEquals(FAILURE_MESSAGE, finalDecision.failureMessage());
    }

    @Test
    void evaluate_subsequentContentErrors_alwaysReturnContentErrorFinal() {
        // Any count >= 1 results in final (covers legacy data with higher counts)
        for (int count = 1; count <= 5; count++) {
            FailureCounters counters = new FailureCounters(count, 0);

            RetryDecision decision = evaluator.evaluate(
                    DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                    counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

            assertInstanceOf(RetryDecision.ContentErrorFinal.class, decision,
                    "Expected ContentErrorFinal for contentErrorCount=" + count);
        }
    }

    @Test
    void evaluate_contentError_transientCounterIsIrrelevant() {
        // Non-zero transient counter must not affect content error decision
        FailureCounters counters = new FailureCounters(0, 5);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.ContentErrorRetryable.class, decision);
    }

    // -------------------------------------------------------------------------
    // Transient technical error rules
    // -------------------------------------------------------------------------

    @Test
    void evaluate_transientError_maxRetriesTransientOne_firstError_returnsTransientErrorFinal() {
        // maxRetriesTransient=1: counter before=0, after=1=limit → final immediately
        FailureCounters counters = new FailureCounters(0, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorFinal.class, decision,
                "With maxRetriesTransient=1, first transient error must be final");
        RetryDecision.TransientErrorFinal finalDecision = (RetryDecision.TransientErrorFinal) decision;
        assertEquals(FAILURE_CLASS, finalDecision.failureClass());
        assertEquals(FAILURE_MESSAGE, finalDecision.failureMessage());
    }

    @Test
    void evaluate_transientError_maxRetriesTransientTwo_firstError_returnsTransientErrorRetryable() {
        // maxRetriesTransient=2: counter before=0, after=1 < 2 → retryable
        FailureCounters counters = new FailureCounters(0, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 2, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorRetryable.class, decision);
        RetryDecision.TransientErrorRetryable retryable = (RetryDecision.TransientErrorRetryable) decision;
        assertEquals(FAILURE_CLASS, retryable.failureClass());
        assertEquals(FAILURE_MESSAGE, retryable.failureMessage());
    }

    @Test
    void evaluate_transientError_maxRetriesTransientTwo_secondError_returnsTransientErrorFinal() {
        // maxRetriesTransient=2: counter before=1, after=2=limit → final
        FailureCounters counters = new FailureCounters(0, 1);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 2, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorFinal.class, decision,
                "With maxRetriesTransient=2, second transient error must be final");
    }

    @Test
    void evaluate_transientError_maxRetriesTransientThree_firstError_returnsRetryable() {
        FailureCounters counters = new FailureCounters(0, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 3, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorRetryable.class, decision);
    }

    @Test
    void evaluate_transientError_maxRetriesTransientThree_secondError_returnsRetryable() {
        FailureCounters counters = new FailureCounters(0, 1);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 3, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorRetryable.class, decision);
    }

    @Test
    void evaluate_transientError_maxRetriesTransientThree_thirdError_returnsFinal() {
        // counter before=2, after=3=limit → final
        FailureCounters counters = new FailureCounters(0, 2);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 3, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorFinal.class, decision,
                "Third transient error with maxRetriesTransient=3 must be final");
    }

    @Test
    void evaluate_transientError_contentCounterIsIrrelevant() {
        // Non-zero content error counter must not affect transient error decision
        FailureCounters counters = new FailureCounters(1, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 2, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TransientErrorRetryable.class, decision);
    }

    @Test
    void evaluate_transientError_legacyDataWithHigherCounts_finalizesCorrectly() {
        // Existing legacy data may have counter values beyond normal expectations;
        // the evaluator must still apply the threshold check consistently
        FailureCounters counters = new FailureCounters(3, 5);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                counters, 3, FAILURE_CLASS, FAILURE_MESSAGE);

        // counter before=5, after=6 >= 3 → final
        assertInstanceOf(RetryDecision.TransientErrorFinal.class, decision);
    }

    // -------------------------------------------------------------------------
    // Target copy technical error rule
    // -------------------------------------------------------------------------

    @Test
    void evaluate_targetCopyError_returnsTargetCopyWithImmediateRetry() {
        FailureCounters counters = new FailureCounters(0, 0);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TARGET_COPY_TECHNICAL_ERROR,
                counters, 1, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TargetCopyWithImmediateRetry.class, decision);
        RetryDecision.TargetCopyWithImmediateRetry immediate =
                (RetryDecision.TargetCopyWithImmediateRetry) decision;
        assertEquals(FAILURE_MESSAGE, immediate.failureMessage());
        assertEquals(DocumentErrorClassification.TARGET_COPY_TECHNICAL_ERROR.name(),
                immediate.failureClass());
    }

    @Test
    void evaluate_targetCopyError_countersAndMaxRetriesAreIgnored() {
        // Target copy decision is independent of counters and maxRetriesTransient
        FailureCounters counters = new FailureCounters(2, 3);

        RetryDecision decision = evaluator.evaluate(
                DocumentErrorClassification.TARGET_COPY_TECHNICAL_ERROR,
                counters, 5, FAILURE_CLASS, FAILURE_MESSAGE);

        assertInstanceOf(RetryDecision.TargetCopyWithImmediateRetry.class, decision);
    }

    // -------------------------------------------------------------------------
    // Immediate within-run retry decision
    // -------------------------------------------------------------------------

    @Test
    void evaluateImmediateRetry_firstAttempt_returnsAllowed() {
        ImmediateRetryDecision decision = evaluator.evaluateImmediateRetry(true);

        assertEquals(ImmediateRetryDecision.ALLOWED, decision);
    }

    @Test
    void evaluateImmediateRetry_secondAttempt_returnsDenied() {
        ImmediateRetryDecision decision = evaluator.evaluateImmediateRetry(false);

        assertEquals(ImmediateRetryDecision.DENIED, decision);
    }

    // -------------------------------------------------------------------------
    // Guard conditions
    // -------------------------------------------------------------------------

    @Test
    void evaluate_throwsWhenMaxRetriesTransientIsZero() {
        FailureCounters counters = FailureCounters.zero();

        assertThrows(IllegalArgumentException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                        counters, 0, FAILURE_CLASS, FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenMaxRetriesTransientIsNegative() {
        FailureCounters counters = FailureCounters.zero();

        assertThrows(IllegalArgumentException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.TRANSIENT_TECHNICAL_ERROR,
                        counters, -1, FAILURE_CLASS, FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenErrorClassIsNull() {
        assertThrows(NullPointerException.class, () ->
                evaluator.evaluate(null, FailureCounters.zero(), 1,
                        FAILURE_CLASS, FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenCountersAreNull() {
        assertThrows(NullPointerException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                        null, 1, FAILURE_CLASS, FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenFailureClassIsNull() {
        assertThrows(NullPointerException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                        FailureCounters.zero(), 1, null, FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenFailureClassIsBlank() {
        assertThrows(IllegalArgumentException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                        FailureCounters.zero(), 1, "  ", FAILURE_MESSAGE));
    }

    @Test
    void evaluate_throwsWhenFailureMessageIsNull() {
        assertThrows(NullPointerException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                        FailureCounters.zero(), 1, FAILURE_CLASS, null));
    }

    @Test
    void evaluate_throwsWhenFailureMessageIsBlank() {
        assertThrows(IllegalArgumentException.class, () ->
                evaluator.evaluate(
                        DocumentErrorClassification.DETERMINISTIC_CONTENT_ERROR,
                        FailureCounters.zero(), 1, FAILURE_CLASS, "   "));
    }
}
