package at.procon.eventhub.tachographfilesession.service;

import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.service.EventDetailsFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.assertj.core.api.Assertions.assertThat;

import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;

class DriverTimelineReusableProjectionBuilderTest {

    private final DriverTimelineBuilder legacyBuilder = new DriverTimelineBuilder();
    private final DriverTimelineReusableProjectionBuilder reusableBuilder =
            new DriverTimelineReusableProjectionBuilder(legacyBuilder, new EventHubProperties());

    @Test
    void matchesLegacyDrivingDerivedProjectionChain() {
        DriverExtractionSession driver = new DriverExtractionSession(
                "12:123",
                null,
                null,
                List.of(),
                List.of(),
                List.of(
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                100L,
                                200L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-1"
                        ),
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-2",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T02:00:00Z"),
                                201L,
                                260L,
                                "12:REG-2",
                                "VIN-2",
                                "vu-2"
                        )
                ),
                List.of(
                        new ExtractedCardActivityInterval(
                                "ACT-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "a"
                        ),
                        new ExtractedCardActivityInterval(
                                "ACT-2",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T00:30:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-2",
                                "VIN-2",
                                "b"
                        )
                ),
                List.of(),
                List.of()
        );
        TachographFileSession session = new TachographFileSession(
                UUID.randomUUID(),
                new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null),
                Map.of(driver.driverKey(), driver),
                new ExtractionStats(1, 2, 2, 1, 1, 0),
                List.of(),
                Instant.now(),
                Instant.now().plus(4, ChronoUnit.HOURS)
        );

        ResolvedDriverTimeline timeline = legacyBuilder.build(session, driver);
        List<TachographEsperDrivingInterruptionIntervalEvent> legacyDrivingInterruptions =
                legacyBuilder.buildEsperDrivingInterruptionIntervalEvents(session.sessionId(), driver.driverKey(), timeline, 3);
        List<TachographEsperDrivingInterruptionIntervalEvent> legacyDailyWeeklyRestCandidates =
                legacyBuilder.buildEsperDailyWeeklyRestCandidateIntervalEvents(legacyDrivingInterruptions, 720);
        List<TachographEsperDrivingInterruptionIntervalEvent> legacyDrivingInterruptionVehicleChanges =
                legacyBuilder.buildEsperDrivingInterruptionVehicleChangeIntervalEvents(legacyDailyWeeklyRestCandidates);
        List<TachographEsperVuCardAbsentIntervalEvent> legacyVuCardAbsentIntervals =
                legacyBuilder.buildEsperVuCardAbsentIntervalEvents(timeline);
        List<TachographEsperPotentialHomeOvernightStayIntervalEvent> legacyPotentialHomeOvernightStays =
                legacyBuilder.buildEsperPotentialHomeOvernightStayIntervalEvents(
                        legacyDrivingInterruptionVehicleChanges,
                        legacyVuCardAbsentIntervals
                );

        TachographEsperDrivingDerivedProjectionBundle reusableBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundle(
                        session.sessionId(),
                        driver.driverKey(),
                        timeline,
                        3,
                        720
                );

        assertThat(reusableBundle.drivingInterruptionIntervals()).containsExactlyElementsOf(legacyDrivingInterruptions);
        assertThat(reusableBundle.dailyWeeklyRestCandidateIntervals()).containsExactlyElementsOf(legacyDailyWeeklyRestCandidates);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
        TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent coverageInterval =
                reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0);
        assertThat(coverageInterval.cardAbsentCoveragePercent()).isCloseTo(99.998d, org.assertj.core.data.Offset.offset(0.001d));
        assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).containsExactlyElementsOf(legacyDrivingInterruptionVehicleChanges);
        assertThat(reusableBundle.vuCardAbsentIntervals()).containsExactlyElementsOf(legacyVuCardAbsentIntervals);
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).hasSize(1);
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).startedAt())
                .isEqualTo(legacyPotentialHomeOvernightStays.get(0).startedAt());
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endedAt())
                .isEqualTo(legacyPotentialHomeOvernightStays.get(0).endedAt());
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).cardAbsentCoveragePercent())
                .isEqualTo(legacyPotentialHomeOvernightStays.get(0).cardAbsentCoveragePercent());
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).beginBoundaryOdometerKm())
                .isEqualTo(200L);
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals().get(0).endBoundaryOdometerKm())
                .isEqualTo(201L);
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).isEmpty();
    }

    @Test
    void buildsPotentialInVehicleOvernightStayIntervalsFromVehicleUnchangedCoveredRest() {
        DriverExtractionSession driver = new DriverExtractionSession(
                "12:123",
                null,
                null,
                List.of(),
                List.of(),
                List.of(
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T01:00:00Z"),
                                100L,
                                260L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-1"
                        )
                ),
                List.of(
                        new ExtractedCardActivityInterval(
                                "ACT-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "a"
                        ),
                        new ExtractedCardActivityInterval(
                                "ACT-2",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T00:30:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "b"
                        )
                ),
                List.of(
                        new ExtractedSupportEvent(
                                "SUP-1",
                                "12:123",
                                OffsetDateTime.parse("2026-05-01T09:45:00Z"),
                                "POSITION",
                                "POSITION_RECORDED",
                                "SNAPSHOT",
                                null,
                                "12:REG-1",
                                "VIN-1",
                                null,
                                null,
                                null,
                                null,
                                null,
                                new java.math.BigDecimal("48.2082"),
                                new java.math.BigDecimal("16.3738"),
                                null,
                                150L,
                                null,
                                null,
                                null,
                                "geo-1"
                        ),
                        new ExtractedSupportEvent(
                                "SUP-2",
                                "12:123",
                                OffsetDateTime.parse("2026-05-02T00:15:00Z"),
                                "POSITION",
                                "POSITION_RECORDED",
                                "SNAPSHOT",
                                null,
                                "12:REG-1",
                                "VIN-1",
                                null,
                                null,
                                null,
                                null,
                                null,
                                new java.math.BigDecimal("48.2090"),
                                new java.math.BigDecimal("16.3740"),
                                null,
                                150L,
                                null,
                                null,
                                null,
                                "geo-2"
                        )
                ),
                List.of()
        );
        TachographFileSession session = new TachographFileSession(
                UUID.randomUUID(),
                new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null),
                Map.of(driver.driverKey(), driver),
                new ExtractionStats(1, 2, 1, 1, 1, 0),
                List.of(),
                Instant.now(),
                Instant.now().plus(4, ChronoUnit.HOURS)
        );

        ResolvedDriverTimeline timeline = legacyBuilder.build(session, driver);
        TachographEsperDrivingDerivedProjectionBundle reusableBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundle(
                        session.sessionId(),
                        driver.driverKey(),
                        timeline,
                        3,
                        720
        );

        assertThat(reusableBundle.drivingInterruptionVehicleChangeIntervals()).isEmpty();
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent())
                .isEqualTo(0.0d);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoEventId())
                .isEqualTo("SUP-1");
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoEvent())
                .extracting("eventId", "eventDomain")
                .containsExactly("SUP-1", "POSITION");
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoEventId())
                .isEqualTo("SUP-2");
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoEvent())
                .extracting("eventId", "eventDomain")
                .containsExactly("SUP-2", "POSITION");
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginGeoDistanceSeconds())
                .isEqualTo(900L);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endGeoDistanceSeconds())
                .isEqualTo(900L);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginBoundaryOdometerKm())
                .isEqualTo(100L);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).endBoundaryOdometerKm())
                .isEqualTo(260L);
        assertThat(reusableBundle.dailyWeeklyRestCandidateCoverageIntervals().get(0).geoEvidenceMovementCategory())
                .isEqualTo("STATIONARY");
        assertThat(reusableBundle.potentialHomeOvernightStayIntervals()).isEmpty();
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals()).hasSize(1);
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).startedAt())
                .isEqualTo(OffsetDateTime.parse("2026-05-01T10:00:00Z"));
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).endedAt())
                .isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEventId())
                .isEqualTo("SUP-1");
        assertThat(reusableBundle.potentialInVehicleOvernightStayIntervals().get(0).beginGeoEvent())
                .extracting("eventId", "eventDomain")
                .containsExactly("SUP-1", "POSITION");
    }

    @Test
    void derivesDrivingProjectionFromRawPointEvents() {
        DriverExtractionSession driver = new DriverExtractionSession(
                "12:123",
                null,
                null,
                List.of(),
                List.of(),
                List.of(
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                100L,
                                200L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-1"
                        ),
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-2",
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T12:00:00Z"),
                                200L,
                                250L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-2"
                        ),
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-3",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T02:00:00Z"),
                                251L,
                                300L,
                                "12:REG-2",
                                "VIN-2",
                                "vu-3"
                        )
                ),
                List.of(
                        new ExtractedCardActivityInterval(
                                "ACT-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "a"
                        ),
                        new ExtractedCardActivityInterval(
                                "ACT-2",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T00:30:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-2",
                                "VIN-2",
                                "b"
                        )
                ),
                List.of(),
                List.of()
        );
        TachographFileSession session = new TachographFileSession(
                UUID.randomUUID(),
                new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null),
                Map.of(driver.driverKey(), driver),
                new ExtractionStats(1, 2, 3, 1, 1, 0),
                List.of(),
                Instant.now(),
                Instant.now().plus(4, ChronoUnit.HOURS)
        );

        IntervalBackedDriverTimelineEventBuilder intervalEventBuilder = new IntervalBackedDriverTimelineEventBuilder(
                legacyBuilder,
                new DriverKeyFactory(),
                new VehicleKeyFactory(),
                new EventDetailsFactory(new ObjectMapper())
        );
        RawSourceDriverTimelineEventBuilder rawSourceEventBuilder = new RawSourceDriverTimelineEventBuilder(intervalEventBuilder);
        TachographTimelineEventBundle rawPointEventBundle = rawSourceEventBuilder.buildRawEventBundle(session, driver);

        assertThat(rawPointEventBundle.activityEvents()).hasSize(4);
        assertThat(rawPointEventBundle.vehicleUsageEvents()).hasSize(6);

        ResolvedDriverTimeline javaResolvedTimeline = legacyBuilder.build(session, driver);
        TachographEsperDrivingDerivedProjectionBundle intervalBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundle(
                        session.sessionId(),
                        driver.driverKey(),
                        javaResolvedTimeline,
                        3,
                        720
                );
        TachographEsperDrivingDerivedProjectionBundle eventBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(
                        session.sessionId(),
                        driver.driverKey(),
                        rawPointEventBundle.allEvents(),
                        3,
                        720
                );

        assertThat(eventBundle.drivingInterruptionIntervals())
                .containsExactlyElementsOf(intervalBundle.drivingInterruptionIntervals());
        assertThat(eventBundle.dailyWeeklyRestCandidateIntervals())
                .containsExactlyElementsOf(intervalBundle.dailyWeeklyRestCandidateIntervals());
        assertThat(eventBundle.drivingInterruptionVehicleChangeIntervals())
                .containsExactlyElementsOf(intervalBundle.drivingInterruptionVehicleChangeIntervals());
        assertThat(eventBundle.vuCardAbsentIntervals()).hasSize(1);
        assertThat(eventBundle.vuCardAbsentIntervals().get(0).startedAt())
                .isEqualTo(OffsetDateTime.parse("2026-05-01T12:00:01Z"));
        assertThat(eventBundle.vuCardAbsentIntervals().get(0).endedAt())
                .isEqualTo(OffsetDateTime.parse("2026-05-02T00:00:00Z"));
    }


    @Test
    void eventModeMergesVehicleUsageIntervalsAcrossMidnightContinuation() {
        DriverExtractionSession driver = new DriverExtractionSession(
                "12:123",
                null,
                null,
                List.of(),
                List.of(),
                List.of(
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T23:59:59Z"),
                                100L,
                                240L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-1"
                        ),
                        new ExtractedCardVehicleUsageInterval(
                                "CVU-2",
                                OffsetDateTime.parse("2026-05-02T00:00:00Z"),
                                OffsetDateTime.parse("2026-05-02T02:00:00Z"),
                                240L,
                                280L,
                                "12:REG-1",
                                "VIN-1",
                                "vu-2"
                        )
                ),
                List.of(
                        new ExtractedCardActivityInterval(
                                "ACT-1",
                                OffsetDateTime.parse("2026-05-01T08:00:00Z"),
                                OffsetDateTime.parse("2026-05-01T10:00:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "a"
                        ),
                        new ExtractedCardActivityInterval(
                                "ACT-2",
                                OffsetDateTime.parse("2026-05-02T00:30:00Z"),
                                OffsetDateTime.parse("2026-05-02T01:00:00Z"),
                                "DRIVE",
                                "DRIVER",
                                "INSERTED",
                                "SINGLE",
                                "12:REG-1",
                                "VIN-1",
                                "b"
                        )
                ),
                List.of(),
                List.of()
        );
        TachographFileSession session = new TachographFileSession(
                UUID.randomUUID(),
                new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 2, "42", "b", true, null),
                Map.of(driver.driverKey(), driver),
                new ExtractionStats(1, 2, 2, 1, 1, 0),
                List.of(),
                Instant.now(),
                Instant.now().plus(4, ChronoUnit.HOURS)
        );

        IntervalBackedDriverTimelineEventBuilder intervalEventBuilder = new IntervalBackedDriverTimelineEventBuilder(
                legacyBuilder,
                new DriverKeyFactory(),
                new VehicleKeyFactory(),
                new EventDetailsFactory(new ObjectMapper())
        );
        RawSourceDriverTimelineEventBuilder rawSourceEventBuilder = new RawSourceDriverTimelineEventBuilder(intervalEventBuilder);
        TachographTimelineEventBundle rawPointEventBundle = rawSourceEventBuilder.buildRawEventBundle(session, driver);
        ResolvedDriverTimeline javaResolvedTimeline = legacyBuilder.build(session, driver);

        TachographEsperDrivingDerivedProjectionBundle intervalBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundle(
                        session.sessionId(),
                        driver.driverKey(),
                        javaResolvedTimeline,
                        3,
                        720
                );
        TachographEsperDrivingDerivedProjectionBundle eventBundle =
                reusableBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(
                        session.sessionId(),
                        driver.driverKey(),
                        rawPointEventBundle.allEvents(),
                        3,
                        720
                );

        assertThat(javaResolvedTimeline.vehicleUsageIntervals()).hasSize(1);
        assertThat(javaResolvedTimeline.vehicleUsageIntervals().get(0).from())
                .isEqualTo(OffsetDateTime.parse("2026-05-01T08:00:00Z"));
        assertThat(javaResolvedTimeline.vehicleUsageIntervals().get(0).to())
                .isEqualTo(OffsetDateTime.parse("2026-05-02T02:00:00Z"));
        assertThat(eventBundle.vuCardAbsentIntervals()).isEmpty();
        assertThat(eventBundle.dailyWeeklyRestCandidateCoverageIntervals())
                .containsExactlyElementsOf(intervalBundle.dailyWeeklyRestCandidateCoverageIntervals());
        assertThat(eventBundle.potentialInVehicleOvernightStayIntervals())
                .containsExactlyElementsOf(intervalBundle.potentialInVehicleOvernightStayIntervals());
    }

}
