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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Instant;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerSessionTotals;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStartException;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerState;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStatus;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTrigger;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockException;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerConfig;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerPort;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettings;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsPort;

/**
 * Unit-Tests für {@link DefaultSchedulerControlUseCase}.
 */
@ExtendWith(MockitoExtension.class)
class DefaultSchedulerControlUseCaseTest {

    @Mock
    private SchedulerPort schedulerPort;
    @Mock
    private ConfigurationFileLockPort lockPort;
    @Mock
    private SchedulerSettingsPort settingsPort;
    @Mock
    private BatchRunTrigger batchRunTrigger;

    @BeforeEach
    void setUp() {
        when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(180));
    }

    @Test
    void getStatus_returnsInitialStatusOnCreation() {
        DefaultSchedulerControlUseCase useCase = createUseCase();

        SchedulerStatus status = useCase.getStatus();

        assertThat(status.state()).isEqualTo(SchedulerState.STOPPED);
        assertThat(status.lastRunEndedAt()).isEmpty();
        assertThat(status.lastRunSummary()).isEmpty();
        assertThat(status.nextTickAt()).isEmpty();
        assertThat(status.lastError()).isEmpty();
        assertThat(status.sessionTotals()).isEmpty();
    }

    @Test
    void start_initializesSessionTotalsToZero() {
        DefaultSchedulerControlUseCase useCase = createUseCase();

        useCase.start();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.sessionTotals()).contains(SchedulerSessionTotals.zero());
    }

    @Test
    void tick_started_accumulatesIntoSessionTotals() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(2, 1, 4)))
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(3, 0, 0)));

        useCase.executeWrappedTick();
        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(5, 1));
    }

    @Test
    void tick_skippedBusy_doesNotChangeSessionTotals() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(2, 1, 0)))
                .thenReturn(new BatchRunTriggerResult.SkippedBusy());

        useCase.executeWrappedTick();
        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(2, 1));
    }

    @Test
    void tick_failed_doesNotChangeSessionTotals() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(1, 1, 0)))
                .thenReturn(new BatchRunTriggerResult.Failed("Fehler", "tech"));

        useCase.executeWrappedTick();
        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(1, 1));
    }

    @Test
    void stop_keepsSessionTotalsVisible() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(7, 2, 0)));
        useCase.executeWrappedTick();

        useCase.stop();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.state()).isEqualTo(SchedulerState.STOPPED);
        assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(7, 2));
    }

    @Test
    void start_afterPreviousSession_resetsSessionTotalsToZero() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(5, 3, 0)));
        useCase.executeWrappedTick();
        useCase.stop();
        assertThat(useCase.getStatus().sessionTotals())
                .contains(new SchedulerSessionTotals(5, 3));

        useCase.start();

        assertThat(useCase.getStatus().sessionTotals()).contains(SchedulerSessionTotals.zero());
    }

    @Test
    void constructor_loadsSettingsIntervalSeconds() {
        when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(300));
        DefaultSchedulerControlUseCase useCase = createUseCase();

        useCase.start();
        ArgumentCaptor<SchedulerConfig> configCaptor = ArgumentCaptor.forClass(SchedulerConfig.class);
        verify(schedulerPort).startScheduler(configCaptor.capture(), any());
        assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(300);
    }

    @Test
    void constructor_clampsIntervalBelowMinimumTo30() {
        when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(10));
        DefaultSchedulerControlUseCase useCase = createUseCase();

        useCase.start();
        ArgumentCaptor<SchedulerConfig> configCaptor = ArgumentCaptor.forClass(SchedulerConfig.class);
        verify(schedulerPort).startScheduler(configCaptor.capture(), any());
        assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(30);
    }

    @Test
    void start_setsStateToRunningIdle() {
        DefaultSchedulerControlUseCase useCase = createUseCase();

        useCase.start();

        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_IDLE);
    }

    @Test
    void start_setsNextTickAt() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        Instant before = Instant.now();

        useCase.start();

        Instant after = Instant.now();
        Optional<Instant> nextTickAt = useCase.getStatus().nextTickAt();
        assertThat(nextTickAt).isPresent();
        assertThat(nextTickAt.get()).isAfterOrEqualTo(before.plusSeconds(179));
        assertThat(nextTickAt.get()).isBeforeOrEqualTo(after.plusSeconds(181));
    }

    @Test
    void start_acquiresLock() {
        createUseCase().start();

        verify(lockPort).acquireLock();
    }

    @Test
    void start_startsSchedulerWithCorrectInterval() {
        createUseCase().start();

        ArgumentCaptor<SchedulerConfig> configCaptor = ArgumentCaptor.forClass(SchedulerConfig.class);
        verify(schedulerPort).startScheduler(configCaptor.capture(), any());
        assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(180);
    }

    @Test
    void start_whenAlreadyActive_isIdempotent() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        assertThatCode(useCase::start).doesNotThrowAnyException();
        verify(lockPort).acquireLock();
        verify(schedulerPort).startScheduler(any(), any());
    }

    @Test
    void start_rollsBackToStoppedWhenAcquireLockFails() {
        doThrow(new ConfigurationFileLockException("Lock nicht verfügbar"))
                .when(lockPort).acquireLock();

        DefaultSchedulerControlUseCase useCase = createUseCase();

        assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
        verify(schedulerPort, never()).startScheduler(any(), any());
    }

    @Test
    void start_rollsBackToStoppedWhenSchedulerPortFails() {
        doThrow(new RuntimeException("Executor-Startfehler"))
                .when(schedulerPort).startScheduler(any(), any());

        DefaultSchedulerControlUseCase useCase = createUseCase();

        assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
        verify(lockPort).releaseLock();
    }

    @Test
    void stop_whenRunningIdle_stopsImmediately() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        useCase.stop();

        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
    }

    @Test
    void stop_releasesLock() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        useCase.stop();

        verify(lockPort).releaseLock();
    }

    @Test
    void stop_stopsSchedulerAdapter() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        useCase.stop();

        verify(schedulerPort).stopScheduler();
    }

    @Test
    void stop_clearsNextTickAt() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        useCase.stop();

        assertThat(useCase.getStatus().nextTickAt()).isEmpty();
    }

    @Test
    void stop_whenAlreadyStopped_isIdempotent() {
        DefaultSchedulerControlUseCase useCase = createUseCase();

        assertThatCode(useCase::stop).doesNotThrowAnyException();
        verify(schedulerPort, never()).stopScheduler();
    }

    @Test
    void stop_calledTwice_isIdempotent() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        useCase.stop();

        assertThatCode(useCase::stop).doesNotThrowAnyException();
        verify(schedulerPort).stopScheduler();
    }

    @Test
    void stop_whenBatchRunning_setsStoppingBatchActiveAndDefersCleaup() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
            assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
            useCase.stop();
            assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPING_BATCH_ACTIVE);
            verify(lockPort, never()).releaseLock();
            return new BatchRunTriggerResult.SkippedBusy();
        });

        useCase.executeWrappedTick();

        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
        verify(lockPort).releaseLock();
    }

    @Test
    void tick_started_setsRunningIdleWithLastRunData() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        Instant endedAt = Instant.now();
        RunSummary summary = new RunSummary(3, 1, 2);
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(endedAt, summary));

        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.state()).isEqualTo(SchedulerState.RUNNING_IDLE);
        assertThat(status.lastRunEndedAt()).contains(endedAt);
        assertThat(status.lastRunSummary()).contains(summary);
        assertThat(status.lastError()).isEmpty();
        assertThat(status.nextTickAt()).isPresent();
    }

    @Test
    void tick_started_clearsLastError() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Failed("GUI-Fehler", "technisch"));
        useCase.executeWrappedTick();
        assertThat(useCase.getStatus().lastError()).isPresent();

        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp()));
        useCase.executeWrappedTick();

        assertThat(useCase.getStatus().lastError()).isEmpty();
    }

    @Test
    void tick_skippedBusy_setsRunningIdleWithoutUpdatingLastRun() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun()).thenReturn(new BatchRunTriggerResult.SkippedBusy());

        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.state()).isEqualTo(SchedulerState.RUNNING_IDLE);
        assertThat(status.lastRunEndedAt()).isEmpty();
        assertThat(status.lastRunSummary()).isEmpty();
        assertThat(status.nextTickAt()).isPresent();
    }

    @Test
    void tick_skippedBusy_preservesLastError() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Failed("Vorheriger Fehler", "technisch"));
        useCase.executeWrappedTick();
        assertThat(useCase.getStatus().lastError()).isPresent();

        when(batchRunTrigger.triggerRun()).thenReturn(new BatchRunTriggerResult.SkippedBusy());
        useCase.executeWrappedTick();

        assertThat(useCase.getStatus().lastError()).isPresent();
        assertThat(useCase.getStatus().lastError().get()).isEqualTo("Vorheriger Fehler");
    }

    @Test
    void tick_failed_setsRunningIdleWithLastError() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun())
                .thenReturn(new BatchRunTriggerResult.Failed("Benutzer-Fehlermeldung", "Technische Details"));

        useCase.executeWrappedTick();

        SchedulerStatus status = useCase.getStatus();
        assertThat(status.state()).isEqualTo(SchedulerState.RUNNING_IDLE);
        assertThat(status.lastError()).contains("Benutzer-Fehlermeldung");
        assertThat(status.nextTickAt()).isPresent();
    }

    @Test
    void tick_setsRunningBatchActiveBeforeDelegating() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
            assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
            return new BatchRunTriggerResult.SkippedBusy();
        });

        useCase.executeWrappedTick();
    }

    @Test
    void tick_whenStoppingBatchActive_setsStoppedAndCleansUp() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();

        when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
            useCase.stop();
            return new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp());
        });

        useCase.executeWrappedTick();

        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
        assertThat(useCase.getStatus().nextTickAt()).isEmpty();
        verify(lockPort).releaseLock();
    }

    @Test
    void tick_whenStoppingBatchActive_setsLastRunDataBeforeStopping() {
        DefaultSchedulerControlUseCase useCase = createUseCase();
        useCase.start();
        Instant endedAt = Instant.now();
        RunSummary summary = new RunSummary(1, 0, 0);

        when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
            useCase.stop();
            return new BatchRunTriggerResult.Started(endedAt, summary);
        });

        useCase.executeWrappedTick();

        assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
        assertThat(useCase.getStatus().lastRunEndedAt()).contains(endedAt);
        assertThat(useCase.getStatus().lastRunSummary()).contains(summary);
    }

    private DefaultSchedulerControlUseCase createUseCase() {
        return new DefaultSchedulerControlUseCase(schedulerPort, lockPort, settingsPort, batchRunTrigger);
    }
}
