package de.gecheckt.pdf.umbenenner.adapter.in.scheduler;

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

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerConfig;

/**
 * Unit- und Integrationstests für {@link ScheduledExecutorServiceSchedulerAdapter}.
 * <p>
 * Teststrategien:
 * <ul>
 *   <li>Lifecycle-Tests (Start, Stop, Idempotenz) nutzen {@link CountDownLatch}
 *       für deterministische Synchronisation ohne {@code Thread.sleep}.</li>
 *   <li>Tick-Logik-Tests ({@code onTick}) rufen die package-private Methode
 *       direkt auf und setzen {@code currentTrigger} ohne Executor.</li>
 * </ul>
 */
class ScheduledExecutorServiceSchedulerAdapterTest {

    // =========================================================================
    // Lifecycle: startScheduler
    // =========================================================================

    @Test
    void startScheduler_triggersFirstTickImmediately() throws Exception {
        List<BatchRunTriggerResult> results = new CopyOnWriteArrayList<>();
        CountDownLatch latch = new CountDownLatch(1);

        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {
                    results.add(result);
                    latch.countDown();
                });

        SchedulerConfig config = new SchedulerConfig(3600);
        adapter.startScheduler(config, () -> new BatchRunTriggerResult.SkippedBusy());
        try {
            assertThat(latch.await(5, TimeUnit.SECONDS))
                    .as("Erster Tick muss innerhalb von 5 Sekunden ausgelöst werden")
                    .isTrue();
            assertThat(results).hasSize(1);
            assertThat(results.get(0)).isInstanceOf(BatchRunTriggerResult.SkippedBusy.class);
        } finally {
            adapter.stopScheduler();
        }
    }

    @Test
    void startScheduler_isIdempotent_secondCallDoesNotCreateSecondExecutor() throws Exception {
        List<BatchRunTriggerResult> results = new CopyOnWriteArrayList<>();
        CountDownLatch latch = new CountDownLatch(1);

        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {
                    results.add(result);
                    latch.countDown();
                });

        SchedulerConfig config = new SchedulerConfig(3600);
        adapter.startScheduler(config, () -> new BatchRunTriggerResult.SkippedBusy());
        adapter.startScheduler(config, () -> new BatchRunTriggerResult.SkippedBusy()); // no-op

        try {
            latch.await(5, TimeUnit.SECONDS);
            // Kurze Wartezeit: ein zweiter Executor würde sofort einen zweiten Tick feuern
            Thread.sleep(100);
            assertThat(results)
                    .as("Nur ein Executor → genau ein sofortiger Tick mit Intervall 3600s")
                    .hasSize(1);
        } finally {
            adapter.stopScheduler();
        }
    }

    @Test
    void startScheduler_afterStop_canBeRestartedWithNewTrigger() throws Exception {
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {});

        CountDownLatch firstLatch = new CountDownLatch(1);
        CountDownLatch secondLatch = new CountDownLatch(1);
        SchedulerConfig config = new SchedulerConfig(3600);

        adapter.startScheduler(config, () -> {
            firstLatch.countDown();
            return new BatchRunTriggerResult.SkippedBusy();
        });
        firstLatch.await(5, TimeUnit.SECONDS);
        adapter.stopScheduler();

        List<BatchRunTriggerResult> secondResults = new CopyOnWriteArrayList<>();
        adapter.startScheduler(config, () -> {
            BatchRunTriggerResult r =
                    new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp());
            secondResults.add(r);
            secondLatch.countDown();
            return r;
        });

        try {
            assertThat(secondLatch.await(5, TimeUnit.SECONDS))
                    .as("Zweiter Start muss einen Tick auslösen")
                    .isTrue();
            assertThat(secondResults.get(0)).isInstanceOf(BatchRunTriggerResult.Started.class);
        } finally {
            adapter.stopScheduler();
        }
    }

    // =========================================================================
    // Lifecycle: stopScheduler
    // =========================================================================

    @Test
    void stopScheduler_withoutPriorStart_doesNotThrow() {
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {});

        assertThatCode(adapter::stopScheduler).doesNotThrowAnyException();
    }

    @Test
    void stopScheduler_calledTwice_isIdempotent() throws Exception {
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {});

        CountDownLatch latch = new CountDownLatch(1);
        adapter.startScheduler(new SchedulerConfig(3600), () -> {
            latch.countDown();
            return new BatchRunTriggerResult.SkippedBusy();
        });
        latch.await(5, TimeUnit.SECONDS);

        adapter.stopScheduler();
        assertThatCode(adapter::stopScheduler)
                .as("Zweiter Stop-Aufruf darf keine Ausnahme werfen")
                .doesNotThrowAnyException();
    }

    // =========================================================================
    // Tick-Logik: onTick (direkte Aufrufe, kein Executor)
    // =========================================================================

    @Test
    void onTick_whenTriggerIsNull_doesNotCallConsumer() {
        List<BatchRunTriggerResult> results = new ArrayList<>();
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(results::add);

        // Kein startScheduler → currentTrigger ist null
        adapter.onTick();

        assertThat(results).isEmpty();
    }

    @Test
    void onTick_whenTriggerReturnsSkippedBusy_passesResultToConsumer() {
        List<BatchRunTriggerResult> results = new ArrayList<>();
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(results::add);

        adapter.currentTrigger.set(() -> new BatchRunTriggerResult.SkippedBusy());
        adapter.onTick();

        assertThat(results).hasSize(1);
        assertThat(results.get(0)).isInstanceOf(BatchRunTriggerResult.SkippedBusy.class);
    }

    @Test
    void onTick_whenTriggerReturnsStarted_passesResultToConsumer() {
        List<BatchRunTriggerResult> results = new ArrayList<>();
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(results::add);

        Instant now = Instant.now();
        RunSummary summary = new RunSummary(2, 1, 0);
        adapter.currentTrigger.set(() -> new BatchRunTriggerResult.Started(now, summary));
        adapter.onTick();

        assertThat(results).hasSize(1);
        BatchRunTriggerResult.Started started =
                (BatchRunTriggerResult.Started) results.get(0);
        assertThat(started.endedAt()).isEqualTo(now);
        assertThat(started.summary()).isEqualTo(summary);
    }

    @Test
    void onTick_whenTriggerThrowsException_exceptionIsSwallowed() {
        List<BatchRunTriggerResult> results = new ArrayList<>();
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(results::add);

        adapter.currentTrigger.set(() -> {
            throw new RuntimeException("Simulierter Trigger-Fehler");
        });

        assertThatCode(adapter::onTick)
                .as("Ausnahme im Trigger darf nicht aus onTick propagieren")
                .doesNotThrowAnyException();
        assertThat(results)
                .as("Consumer darf nicht aufgerufen werden, wenn der Trigger wirft")
                .isEmpty();
    }

    @Test
    void onTick_whenConsumerThrowsException_exceptionIsSwallowed() {
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(result -> {
                    throw new RuntimeException("Simulierter Consumer-Fehler");
                });

        adapter.currentTrigger.set(() -> new BatchRunTriggerResult.SkippedBusy());

        assertThatCode(adapter::onTick)
                .as("Ausnahme im Consumer darf nicht aus onTick propagieren")
                .doesNotThrowAnyException();
    }

    @Test
    void onTick_calledMultipleTimes_passesEachResultToConsumer() {
        List<BatchRunTriggerResult> results = new ArrayList<>();
        ScheduledExecutorServiceSchedulerAdapter adapter =
                new ScheduledExecutorServiceSchedulerAdapter(results::add);

        adapter.currentTrigger.set(() -> new BatchRunTriggerResult.SkippedBusy());
        adapter.onTick();
        adapter.onTick();
        adapter.onTick();

        assertThat(results).hasSize(3);
    }
}
