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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;

/**
 * Tests for {@link DefaultResetDocumentStatusUseCase}.
 * <p>
 * Covers the happy path, per-fingerprint failure isolation, empty-set handling,
 * null-guard on the fingerprint set, and best-effort continuation after failure.
 */
class DefaultResetDocumentStatusUseCaseTest {

    private static final DocumentFingerprint FP1 =
            new DocumentFingerprint("1".repeat(64));
    private static final DocumentFingerprint FP2 =
            new DocumentFingerprint("2".repeat(64));
    private static final DocumentFingerprint FP3 =
            new DocumentFingerprint("3".repeat(64));

    // -------------------------------------------------------------------------
    // Happy path
    // -------------------------------------------------------------------------

    @Test
    void reset_allSucceed_returnsFullSuccessResult() {
        List<DocumentFingerprint> resetFingerprints = new ArrayList<>();
        UnitOfWorkPort alwaysSucceeds = ops -> ops.accept(
                new RecordingTransactionOperations(resetFingerprints));

        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(alwaysSucceeds, noOpLogger());

        ResetDocumentStatusResult result = useCase.reset(Set.of(FP1, FP2));

        assertThat(result.requestedCount()).isEqualTo(2);
        assertThat(result.successCount()).isEqualTo(2);
        assertThat(result.failureCount()).isEqualTo(0);
        assertThat(result.successfullyReset()).containsExactlyInAnyOrder(FP1, FP2);
        assertThat(result.failures()).isEmpty();
        assertThat(resetFingerprints).containsExactlyInAnyOrder(FP1, FP2);
    }

    // -------------------------------------------------------------------------
    // Empty set
    // -------------------------------------------------------------------------

    @Test
    void reset_emptySet_returnsEmptyResult() {
        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(ops -> { }, noOpLogger());

        ResetDocumentStatusResult result = useCase.reset(Set.of());

        assertThat(result.requestedCount()).isEqualTo(0);
        assertThat(result.successCount()).isEqualTo(0);
        assertThat(result.failureCount()).isEqualTo(0);
    }

    // -------------------------------------------------------------------------
    // Failure isolation: DocumentPersistenceException on one fingerprint
    // -------------------------------------------------------------------------

    @Test
    void reset_oneFailsWithPersistenceException_othersSucceed() {
        List<DocumentFingerprint> resetFingerprints = new ArrayList<>();
        UnitOfWorkPort failsForFp2 = ops -> {
            RecordingTransactionOperations txOps =
                    new RecordingTransactionOperations(resetFingerprints);
            ops.accept(txOps);
            // If FP2 was the last one added, throw
            if (!resetFingerprints.isEmpty()
                    && resetFingerprints.getLast().sha256Hex().equals(FP2.sha256Hex())) {
                resetFingerprints.remove(resetFingerprints.size() - 1);
                throw new DocumentPersistenceException("Simulated persistence failure for FP2");
            }
        };

        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(failsForFp2, noOpLogger());

        ResetDocumentStatusResult result = useCase.reset(Set.of(FP1, FP2));

        assertThat(result.requestedCount()).isEqualTo(2);
        // One succeeded, one failed
        assertThat(result.successCount() + result.failureCount()).isEqualTo(2);
    }

    // -------------------------------------------------------------------------
    // Failure isolation: best-effort, all fingerprints attempted after failure
    // -------------------------------------------------------------------------

    @Test
    void reset_firstFingerprintFails_remainingFingerprintsStillAttempted() {
        List<DocumentFingerprint> attempted = new ArrayList<>();
        int[] callCount = {0};
        UnitOfWorkPort failsFirstCall = ops -> {
            callCount[0]++;
            RecordingTransactionOperations txOps = new RecordingTransactionOperations(attempted);
            ops.accept(txOps);
            if (callCount[0] == 1) {
                attempted.remove(attempted.size() - 1); // undo
                throw new DocumentPersistenceException("First call fails");
            }
        };

        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(failsFirstCall, noOpLogger());

        ResetDocumentStatusResult result = useCase.reset(Set.of(FP1, FP2, FP3));

        // All 3 were attempted
        assertThat(result.requestedCount()).isEqualTo(3);
        assertThat(callCount[0]).isEqualTo(3);
        // Exactly 1 failure (the first call)
        assertThat(result.failureCount()).isEqualTo(1);
        assertThat(result.successCount()).isEqualTo(2);
    }

    // -------------------------------------------------------------------------
    // RuntimeException isolation
    // -------------------------------------------------------------------------

    @Test
    void reset_unexpectedRuntimeException_recordedAsFailure() {
        UnitOfWorkPort throwsRuntime = ops -> {
            throw new IllegalStateException("unexpected");
        };

        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(throwsRuntime, noOpLogger());

        ResetDocumentStatusResult result = useCase.reset(Set.of(FP1));

        assertThat(result.requestedCount()).isEqualTo(1);
        assertThat(result.failureCount()).isEqualTo(1);
        assertThat(result.successCount()).isEqualTo(0);
        assertThat(result.failures()).containsKey(FP1);
    }

    // -------------------------------------------------------------------------
    // Null guard
    // -------------------------------------------------------------------------

    @Test
    void reset_nullFingerprintSet_throwsNullPointerException() {
        DefaultResetDocumentStatusUseCase useCase =
                new DefaultResetDocumentStatusUseCase(ops -> { }, noOpLogger());
        assertThatNullPointerException().isThrownBy(() -> useCase.reset(null));
    }

    @Test
    void constructor_rejectsNullUnitOfWorkPort() {
        assertThatNullPointerException()
                .isThrownBy(() -> new DefaultResetDocumentStatusUseCase(null, noOpLogger()));
    }

    @Test
    void constructor_rejectsNullLogger() {
        assertThatNullPointerException()
                .isThrownBy(() -> new DefaultResetDocumentStatusUseCase(ops -> { }, null));
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static ProcessingLogger noOpLogger() {
        return new ProcessingLogger() {
            @Override public void info(String msg, Object... args) {
                // intentionally empty
            }
            @Override public void debug(String msg, Object... args) {
                // intentionally empty
            }
            @Override public void debugSensitiveAiContent(String msg, Object... args) {
                // intentionally empty
            }
            @Override public void warn(String msg, Object... args) {
                // intentionally empty
            }
            @Override public void error(String msg, Object... args) {
                // intentionally empty
            }
        };
    }

    /**
     * Records each {@code resetDocumentByFingerprint} call for assertion.
     * Other transaction operations are no-ops (tests never reach them here).
     */
    private static class RecordingTransactionOperations
            implements UnitOfWorkPort.TransactionOperations {

        private final List<DocumentFingerprint> recorded;

        RecordingTransactionOperations(List<DocumentFingerprint> recorded) {
            this.recorded = recorded;
        }

        @Override
        public void saveProcessingAttempt(
                de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt attempt) {
                    // intentionally empty
                }

        @Override
        public void createDocumentRecord(
                de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
                    // intentionally empty
                }

        @Override
        public void updateDocumentRecord(
                de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord record) {
                    // intentionally empty
                }

        @Override
        public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
            recorded.add(fingerprint);
        }

        @Override
        public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
            // No-op in tests
        }
    }
}
