package at.procon.eventhub.tachographfilesession.service;

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

import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDomain;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.EventLifecycle;
import at.procon.eventhub.dto.EventType;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor;
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
import at.procon.eventhub.service.EventHubEventSorter;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionRequest;
import at.procon.eventhub.tachographfilesession.dto.CreateTachographCompositeSessionResponse;
import at.procon.eventhub.tachographfilesession.dto.TachographCompositeDriverEventsResponse;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

class TachographCompositeSessionServiceTest {

    @Test
    void createsCompositeSessionAndAggregatesDriversAcrossMemberSessions() {
        TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
        TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class);
        DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class);
        UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class);
        TachographCompositeSessionService service = new TachographCompositeSessionService(
                compositeRepository,
                fileSessionRepository,
                eventBuilder,
                reconstructor,
                new EventAcquisitionRecordKeyService(),
                new EventHubEventSorter()
        );

        UUID firstSessionId = UUID.randomUUID();
        UUID secondSessionId = UUID.randomUUID();
        TachographFileSession first = session(
                firstSessionId,
                driver("12:123", "Muster", "Max", "12", "CARD0000000001", 2, 1, List.of(new ExtractionWarning("A", "a", "a"))),
                driver("13:456", "Doe", "Jane", "13", "CARD0000000002", 1, 1, List.of())
        );
        TachographFileSession second = session(
                secondSessionId,
                driver("12:123", "Muster", "Max", "12", "CARD0000000001", 3, 2, List.of()),
                driver("14:789", "Smith", "John", "14", "CARD0000000003", 4, 5, List.of())
        );
        when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first));
        when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second));

        CreateTachographCompositeSessionResponse response = service.createCompositeSession(
                new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId, firstSessionId), "fleet-week-1")
        );

        assertThat(response.session().tenantKey()).isEqualTo("default");
        assertThat(response.session().label()).isEqualTo("fleet-week-1");
        assertThat(response.session().memberSessionIds()).containsExactly(firstSessionId, secondSessionId);
        assertThat(response.session().drivers()).hasSize(3);
        assertThat(response.session().drivers())
                .filteredOn(driver -> driver.driverKey().equals("12:123"))
                .singleElement()
                .satisfies(driverSummary -> {
                    assertThat(driverSummary.activityIntervalCount()).isEqualTo(5);
                    assertThat(driverSummary.cardVehicleUsageIntervalCount()).isEqualTo(3);
                    assertThat(driverSummary.surname()).isEqualTo("Muster");
                    assertThat(driverSummary.firstNames()).isEqualTo("Max");
                    assertThat(driverSummary.cardNumber()).isEqualTo("CARD0000000001");
                });
    }

    @Test
    void mergesDriverEventsAcrossSessionsAndDeduplicatesBySemanticSignature() {
        TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
        TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class);
        DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class);
        UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class);
        TachographCompositeSessionService service = new TachographCompositeSessionService(
                compositeRepository,
                fileSessionRepository,
                eventBuilder,
                reconstructor,
                new EventAcquisitionRecordKeyService(),
                new EventHubEventSorter()
        );

        UUID firstSessionId = UUID.randomUUID();
        UUID secondSessionId = UUID.randomUUID();
        TachographFileSession first = session(firstSessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of()));
        TachographFileSession second = session(secondSessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of()));
        when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first));
        when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second));

        CreateTachographCompositeSessionResponse created = service.createCompositeSession(
                new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId), "driver-merge")
        );

        DriverExtractionSession firstDriver = first.driversByKey().get("12:123");
        DriverExtractionSession secondDriver = second.driversByKey().get("12:123");
        OffsetDateTime firstTimestamp = OffsetDateTime.parse("2026-05-22T08:00:00Z");
        OffsetDateTime secondTimestamp = OffsetDateTime.parse("2026-05-22T09:00:00Z");
        when(eventBuilder.buildEvents(first, firstDriver)).thenReturn(List.of(
                event("duplicate-a", firstTimestamp, EventType.WORK),
                event("unique-a", secondTimestamp, EventType.DRIVE)
        ));
        when(eventBuilder.buildEvents(second, secondDriver)).thenReturn(List.of(
                event("duplicate-b", firstTimestamp, EventType.WORK),
                event("unique-b", secondTimestamp.plusMinutes(30), EventType.BREAK_REST)
        ));

        TachographCompositeDriverEventsResponse response =
                service.getMergedDriverEvents(created.session().compositeSessionId(), "12:123");

        assertThat(response.sourceSessionIds()).containsExactly(firstSessionId, secondSessionId);
        assertThat(response.eventCount()).isEqualTo(3);
        assertThat(response.events()).extracting(EventHubEventDto::externalSourceEventId)
                .containsExactly("duplicate-a", "unique-a", "unique-b");
    }

    @Test
    void reconstructsMergedDriverTimelineFromMergedEventsAndWarnings() {
        TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
        TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class);
        DriverTimelineEventBuilder eventBuilder = Mockito.mock(DriverTimelineEventBuilder.class);
        UnifiedEventTimelineReconstructor reconstructor = Mockito.mock(UnifiedEventTimelineReconstructor.class);
        TachographCompositeSessionService service = new TachographCompositeSessionService(
                compositeRepository,
                fileSessionRepository,
                eventBuilder,
                reconstructor,
                new EventAcquisitionRecordKeyService(),
                new EventHubEventSorter()
        );

        UUID firstSessionId = UUID.randomUUID();
        UUID secondSessionId = UUID.randomUUID();
        DriverExtractionSession firstDriver = driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of(
                new ExtractionWarning("CARD_GAP", "gap", "p1")
        ));
        DriverExtractionSession secondDriver = driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of(
                new ExtractionWarning("DUPLICATE", "duplicate", "p2")
        ));
        TachographFileSession first = session(firstSessionId, List.of(new ExtractionWarning("SESSION_A", "warn-a", "s1")), firstDriver);
        TachographFileSession second = session(secondSessionId, List.of(new ExtractionWarning("SESSION_B", "warn-b", "s2")), secondDriver);
        when(fileSessionRepository.find(firstSessionId)).thenReturn(java.util.Optional.of(first));
        when(fileSessionRepository.find(secondSessionId)).thenReturn(java.util.Optional.of(second));

        CreateTachographCompositeSessionResponse created = service.createCompositeSession(
                new CreateTachographCompositeSessionRequest(List.of(firstSessionId, secondSessionId), null)
        );

        OffsetDateTime occurredAt = OffsetDateTime.parse("2026-05-22T08:00:00Z");
        when(eventBuilder.buildEvents(first, firstDriver)).thenReturn(List.of(event("a", occurredAt, EventType.WORK)));
        when(eventBuilder.buildEvents(second, secondDriver)).thenReturn(List.of(event("b", occurredAt.plusHours(1), EventType.DRIVE)));

        ResolvedDriverTimeline expected = new ResolvedDriverTimeline(
                "COMPOSITE_TACHOGRAPH_FILE_SESSION",
                occurredAt,
                occurredAt.plusHours(1),
                List.of(),
                List.of(),
                List.of(),
                List.of()
        );
        when(reconstructor.reconstruct(any(), eq("12:123"), any(), any(), eq("COMPOSITE_TACHOGRAPH_FILE_SESSION")))
                .thenReturn(expected);

        ResolvedDriverTimeline actual =
                service.getMergedDriverTimeline(created.session().compositeSessionId(), "12:123");

        assertThat(actual).isSameAs(expected);
        ArgumentCaptor<List<EventHubEventDto>> eventsCaptor = ArgumentCaptor.forClass(List.class);
        ArgumentCaptor<List<ExtractionWarning>> warningsCaptor = ArgumentCaptor.forClass(List.class);
        verify(reconstructor).reconstruct(
                eq(created.session().compositeSessionId()),
                eq("12:123"),
                eventsCaptor.capture(),
                warningsCaptor.capture(),
                eq("COMPOSITE_TACHOGRAPH_FILE_SESSION")
        );
        assertThat(eventsCaptor.getValue()).extracting(EventHubEventDto::externalSourceEventId)
                .containsExactly("a", "b");
        assertThat(warningsCaptor.getValue()).containsExactly(
                new ExtractionWarning("SESSION_A", "warn-a", "s1"),
                new ExtractionWarning("CARD_GAP", "gap", "p1"),
                new ExtractionWarning("SESSION_B", "warn-b", "s2"),
                new ExtractionWarning("DUPLICATE", "duplicate", "p2")
        );
    }

    @Test
    void rejectsMissingDriverInCompositeSession() {
        TachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
        TachographFileSessionRepository fileSessionRepository = Mockito.mock(TachographFileSessionRepository.class);
        TachographCompositeSessionService service = new TachographCompositeSessionService(
                compositeRepository,
                fileSessionRepository,
                Mockito.mock(DriverTimelineEventBuilder.class),
                Mockito.mock(UnifiedEventTimelineReconstructor.class),
                new EventAcquisitionRecordKeyService(),
                new EventHubEventSorter()
        );

        UUID sessionId = UUID.randomUUID();
        when(fileSessionRepository.find(sessionId)).thenReturn(java.util.Optional.of(
                session(sessionId, driver("12:123", "Muster", "Max", "12", "CARD0000000001", 0, 0, List.of()))
        ));

        CreateTachographCompositeSessionResponse created = service.createCompositeSession(
                new CreateTachographCompositeSessionRequest(List.of(sessionId), null)
        );

        assertThatThrownBy(() -> service.getMergedDriverEvents(created.session().compositeSessionId(), "missing"))
                .isInstanceOf(DriverNotFoundInCompositeSessionException.class)
                .hasMessageContaining("missing");
    }

    private static TachographFileSession session(UUID sessionId, DriverExtractionSession... drivers) {
        return session(sessionId, List.of(), drivers);
    }

    private static TachographFileSession session(
            UUID sessionId,
            List<ExtractionWarning> warnings,
            DriverExtractionSession... drivers
    ) {
        Map<String, DriverExtractionSession> byKey = java.util.Arrays.stream(drivers)
                .collect(java.util.stream.Collectors.toMap(DriverExtractionSession::driverKey, driver -> driver));
        return new TachographFileSession(
                sessionId,
                new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "sha", 3, "42", "xml", true, null),
                byKey,
                new ExtractionStats(byKey.size(), 0, 0, 0, 0, warnings.size()),
                warnings,
                Instant.parse("2026-05-22T08:00:00Z"),
                Instant.parse("2026-05-22T12:00:00Z")
        );
    }

    private static DriverExtractionSession driver(
            String driverKey,
            String surname,
            String firstNames,
            String cardNation,
            String cardNumber,
            int activityIntervalCount,
            int cardVehicleUsageIntervalCount,
            List<ExtractionWarning> warnings
    ) {
        return new DriverExtractionSession(
                driverKey,
                new ExtractedDriver(driverKey, "SOURCE:" + driverKey, surname, firstNames, LocalDate.parse("1980-01-01"), null, null, null, null),
                new ExtractedDriverCard("CARD:" + driverKey, cardNation, cardNumber, cardNumber + "01", null, null, null, null),
                List.of(),
                List.of(),
                java.util.Collections.nCopies(cardVehicleUsageIntervalCount, null),
                java.util.Collections.nCopies(activityIntervalCount, null),
                List.of(),
                warnings
        );
    }

    private static EventHubEventDto event(String externalSourceEventId, OffsetDateTime occurredAt, EventType eventType) {
        return new EventHubEventDto(
                null,
                externalSourceEventId,
                new DriverRefDto(null, new DriverCardRefDto("12", 12, "CARD0000000001")),
                new VehicleRefDto(null, "VIN00000000000001", null, new VehicleRegistrationRefDto("12", 12, "W-12345A")),
                occurredAt,
                null,
                null,
                eventType == EventType.DRIVE ? EventDomain.DRIVER_ACTIVITY : EventDomain.DRIVER_ACTIVITY,
                eventType,
                eventType == EventType.BREAK_REST ? EventLifecycle.END : EventLifecycle.START,
                null,
                null,
                null,
                null,
                null,
                false,
                null
        );
    }
}
