package at.procon.eventhub.esperpoc.service;

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

import at.procon.eventhub.esperpoc.dto.ActivityIntervalDto;
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodEngineMode;
import at.procon.eventhub.esperpoc.dto.EsperOperatingPeriodRequest;
import at.procon.eventhub.esperpoc.dto.EsperSourceSelectionMode;
import at.procon.eventhub.esperpoc.dto.EsperUnknownTreatmentMode;
import at.procon.eventhub.esperpoc.dto.RawActivityEventDto;
import at.procon.eventhub.esperpoc.persistence.EsperPocActivityRepository;
import at.procon.eventhub.esperpoc.dto.NonDrivingIntervalDto;
import at.procon.eventhub.esperpoc.dto.OperatingPeriodActivityIntervalDto;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class EsperOperatingPeriodEvaluationServiceTest {

    private final EsperOperatingPeriodEngine operatingPeriodEngine = new EsperOperatingPeriodEngine();
    private final EsperOperatingPeriodEvaluationService service = new EsperOperatingPeriodEvaluationService(
            null,
            new EsperDriverActivityEngine(),
            operatingPeriodEngine
    );

    @Test
    void synthesizeUnknownGapsSkipsCoveredBreakRestAndCreatesOnlyUncoveredUnknown() {
        UUID driverId = UUID.randomUUID();
        List<ActivityIntervalDto> intervals = List.of(
                activity(driverId, "WORK", "2026-04-01T08:00:00Z", "2026-04-01T09:00:00Z", "w1", "DRIVER_CARD"),
                activity(driverId, "BREAK_REST", "2026-04-01T09:00:00Z", "2026-04-01T10:00:00Z", "r1", "DRIVER_CARD"),
                activity(driverId, "AVAILABILITY", "2026-04-01T10:00:00Z", "2026-04-01T11:00:00Z", "a1", "DRIVER_CARD"),
                activity(driverId, "WORK", "2026-04-01T12:00:00Z", "2026-04-01T13:00:00Z", "w2", "DRIVER_CARD")
        );

        List<ActivityIntervalDto> evaluationIntervals = service.synthesizeUnknownGaps(intervals, Duration.ZERO);

        assertThat(evaluationIntervals).extracting(ActivityIntervalDto::activityType)
                .containsExactly("WORK", "AVAILABILITY", "UNKNOWN", "WORK");
        assertThat(evaluationIntervals.get(2).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T11:00:00Z"));
        assertThat(evaluationIntervals.get(2).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T12:00:00Z"));
    }

    @Test
    void periodizationSplitsAfterLongUnknownGapAndShortDrivingDoesNotCloseNonDriving() {
        UUID driverId = UUID.randomUUID();
        List<ActivityIntervalDto> evaluationIntervals = List.of(
                activity(driverId, "WORK", "2026-04-01T08:00:00Z", "2026-04-01T09:00:00Z", "w1", "DRIVER_CARD"),
                activity(driverId, "AVAILABILITY", "2026-04-01T10:00:00Z", "2026-04-01T11:00:00Z", "a1", "DRIVER_CARD"),
                activity(driverId, "DRIVE", "2026-04-01T11:00:00Z", "2026-04-01T11:02:00Z", "d1", "DRIVER_CARD"),
                activity(driverId, "WORK", "2026-04-01T11:02:00Z", "2026-04-01T12:00:00Z", "w2", "DRIVER_CARD"),
                unknown(driverId, "2026-04-01T12:00:00Z", "2026-04-01T20:00:00Z"),
                activity(driverId, "DRIVE", "2026-04-01T20:00:00Z", "2026-04-01T20:30:00Z", "d2", "DRIVER_CARD")
        );

        EsperOperatingPeriodEngine.EsperOperatingPeriodEvaluation evaluation = operatingPeriodEngine.evaluate(
                evaluationIntervals,
                Duration.ofHours(7),
                EsperOperatingPeriodEngineMode.STREAM_COLLECTOR
        );

        assertThat(evaluation.periodizedIntervals()).extracting(OperatingPeriodActivityIntervalDto::activityType)
                .containsExactly("WORK", "AVAILABILITY", "DRIVE", "WORK", "DRIVE");
        assertThat(evaluation.closedPeriods()).hasSize(2);
        assertThat(evaluation.closedPeriods().get(0).closedBy()).isEqualTo("UNKNOWN_GAP");
        assertThat(evaluation.closedPeriods().get(1).closedBy()).isEqualTo("FLUSH");

        List<OperatingPeriodActivityIntervalDto> mergedIntervals = service.mergeConsecutiveActivities(
                evaluation.periodizedIntervals(),
                Duration.ZERO
        );
        List<NonDrivingIntervalDto> nonDrivingIntervals = service.buildNonDrivingIntervals(
                mergedIntervals,
                Duration.ofMinutes(3),
                EsperUnknownTreatmentMode.AS_BREAK_REST
        );

        assertThat(nonDrivingIntervals).hasSize(1);
        assertThat(nonDrivingIntervals.get(0).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T08:00:00Z"));
        assertThat(nonDrivingIntervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T12:00:00Z"));
        assertThat(nonDrivingIntervals.get(0).closedBy()).isEqualTo("NEW_OPERATING_PERIOD");
    }

    @Test
    void unknownCanCloseNonDrivingWhenConfiguredAsNonBreak() {
        UUID driverId = UUID.randomUUID();
        List<OperatingPeriodActivityIntervalDto> mergedIntervals = List.of(
                periodized(activity(driverId, "WORK", "2026-04-01T08:00:00Z", "2026-04-01T09:00:00Z", "w1", "DRIVER_CARD"), 1),
                periodized(unknown(driverId, "2026-04-01T09:00:00Z", "2026-04-01T09:30:00Z"), 1),
                periodized(activity(driverId, "AVAILABILITY", "2026-04-01T09:30:00Z", "2026-04-01T10:00:00Z", "a1", "DRIVER_CARD"), 1)
        );

        List<NonDrivingIntervalDto> nonDrivingIntervals = service.buildNonDrivingIntervals(
                mergedIntervals,
                Duration.ofMinutes(3),
                EsperUnknownTreatmentMode.AS_UNKNOWN_NON_BREAK
        );

        assertThat(nonDrivingIntervals).hasSize(2);
        assertThat(nonDrivingIntervals.get(0).closedBy()).isEqualTo("UNKNOWN_GAP");
        assertThat(nonDrivingIntervals.get(0).endedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T09:00:00Z"));
        assertThat(nonDrivingIntervals.get(1).startedAt()).isEqualTo(OffsetDateTime.parse("2026-04-01T09:30:00Z"));
    }

    @Test
    void fullEplModeMatchesStreamCollectorMode() {
        UUID driverId = UUID.randomUUID();
        List<ActivityIntervalDto> evaluationIntervals = List.of(
                activity(driverId, "WORK", "2026-04-01T08:00:00Z", "2026-04-01T09:00:00Z", "w1", "DRIVER_CARD"),
                activity(driverId, "AVAILABILITY", "2026-04-01T10:00:00Z", "2026-04-01T11:00:00Z", "a1", "DRIVER_CARD"),
                unknown(driverId, "2026-04-01T11:00:00Z", "2026-04-01T11:30:00Z"),
                activity(driverId, "WORK", "2026-04-01T11:30:00Z", "2026-04-01T12:00:00Z", "w2", "DRIVER_CARD"),
                unknown(driverId, "2026-04-01T12:00:00Z", "2026-04-01T20:00:00Z"),
                activity(driverId, "DRIVE", "2026-04-01T20:00:00Z", "2026-04-01T20:30:00Z", "d1", "DRIVER_CARD")
        );

        EsperOperatingPeriodEngine.EsperOperatingPeriodEvaluation collectorEvaluation = operatingPeriodEngine.evaluate(
                evaluationIntervals,
                Duration.ofHours(7),
                EsperOperatingPeriodEngineMode.STREAM_COLLECTOR
        );
        EsperOperatingPeriodEngine.EsperOperatingPeriodEvaluation fullEplEvaluation = operatingPeriodEngine.evaluate(
                evaluationIntervals,
                Duration.ofHours(7),
                EsperOperatingPeriodEngineMode.FULL_EPL
        );

        assertThat(fullEplEvaluation.periodizedIntervals())
                .usingRecursiveComparison()
                .isEqualTo(collectorEvaluation.periodizedIntervals());
        assertThat(fullEplEvaluation.closedPeriods())
                .usingRecursiveComparison()
                .isEqualTo(collectorEvaluation.closedPeriods());
    }

    @Test
    void driverCardOnlyModeIgnoresVehicleUnitGapFill() {
        UUID driverId = UUID.randomUUID();
        EsperPocActivityRepository repository = mock(EsperPocActivityRepository.class);
        EsperOperatingPeriodEvaluationService evaluationService = new EsperOperatingPeriodEvaluationService(
                repository,
                new EsperDriverActivityEngine(),
                operatingPeriodEngine
        );
        when(repository.findDriverActivityEvents(eq("default"), eq(driverId), any(), any())).thenReturn(List.of(
                raw(driverId, "DRIVER_CARD", "DRIVE", "START", "2026-04-01T08:00:00Z", "card-1"),
                raw(driverId, "DRIVER_CARD", "DRIVE", "END", "2026-04-01T09:00:00Z", "card-1"),
                raw(driverId, "VEHICLE_UNIT", "DRIVE", "START", "2026-04-01T09:00:00Z", "vu-1"),
                raw(driverId, "VEHICLE_UNIT", "DRIVE", "END", "2026-04-01T10:00:00Z", "vu-1")
        ));

        var result = evaluationService.evaluate(new EsperOperatingPeriodRequest(
                "default",
                driverId,
                OffsetDateTime.parse("2026-04-01T00:00:00Z"),
                OffsetDateTime.parse("2026-04-02T00:00:00Z"),
                24,
                7,
                3,
                0,
                0,
                EsperSourceSelectionMode.DRIVER_CARD_ONLY,
                EsperUnknownTreatmentMode.AS_BREAK_REST,
                EsperOperatingPeriodEngineMode.STREAM_COLLECTOR
        ));

        assertThat(result.rawEventCount()).isEqualTo(2);
        assertThat(result.driverCardRawEventCount()).isEqualTo(2);
        assertThat(result.vehicleUnitRawEventCount()).isZero();
        assertThat(result.driverCardIntervalCount()).isEqualTo(1);
        assertThat(result.vehicleUnitIntervalCount()).isZero();
        assertThat(result.resolvedKnownIntervalCount()).isEqualTo(1);
        assertThat(result.rawEvents()).extracting(RawActivityEventDto::sourceKind).containsOnly("DRIVER_CARD");
    }

    private ActivityIntervalDto activity(
            UUID driverId,
            String activity,
            String from,
            String to,
            String sourceRowId,
            String sourceKind
    ) {
        return ActivityIntervalDto.raw(
                driverId,
                null,
                null,
                activity,
                "DRIVER",
                "INSERTED",
                "KNOWN",
                sourceKind,
                OffsetDateTime.parse(from),
                OffsetDateTime.parse(to),
                sourceRowId
        );
    }

    private ActivityIntervalDto unknown(UUID driverId, String from, String to) {
        return new ActivityIntervalDto(
                driverId,
                null,
                null,
                "UNKNOWN",
                "DRIVER",
                null,
                "UNKNOWN",
                "SYNTHETIC_GAP",
                OffsetDateTime.parse(from),
                OffsetDateTime.parse(to),
                Duration.between(OffsetDateTime.parse(from), OffsetDateTime.parse(to)).getSeconds(),
                null,
                List.of(),
                false,
                "UNKNOWN_GAP"
        );
    }

    private OperatingPeriodActivityIntervalDto periodized(ActivityIntervalDto interval, long operatingPeriodNo) {
        return OperatingPeriodActivityIntervalDto.periodized(
                interval,
                operatingPeriodNo,
                interval.startedAt(),
                false,
                0L
        );
    }

    private RawActivityEventDto raw(
            UUID driverId,
            String sourceKind,
            String eventType,
            String lifecycle,
            String occurredAt,
            String sourceRowId
    ) {
        return new RawActivityEventDto(
                UUID.randomUUID(),
                OffsetDateTime.parse(occurredAt),
                sourceRowId,
                sourceKind + ":" + sourceRowId + ":" + lifecycle,
                sourceKind,
                "DRIVER_CARD".equals(sourceKind) ? "CARD_ACTIVITY" : "VU_ACTIVITY",
                driverId,
                null,
                null,
                eventType,
                lifecycle,
                "DRIVER",
                "INSERTED",
                "KNOWN",
                null
        );
    }
}
