package pl.firmatpp.kierowca.ui

import java.time.LocalDate
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
import pl.firmatpp.kierowca.data.upload.RouteActionType

class DriverUiRulesTest {
    private val today = LocalDate.parse("2026-06-30")

    @Test
    fun allowsPhotoActionsOnlyForTodayRouteDate() {
        assertTrue(canManageRoutePhotos("2026-06-30", today))
        assertFalse(canManageRoutePhotos("2026-06-29", today))
        assertFalse(canManageRoutePhotos("2026-07-01", today))
    }

    @Test
    fun blocksPhotoActionsWhenDateIsInvalid() {
        assertFalse(canManageRoutePhotos("", today))
        assertFalse(canManageRoutePhotos("not-a-date", today))
    }

    @Test
    fun allowsRouteCompletionOnlyForTodayPlannedOrInProgressRoutes() {
        assertTrue(canCompleteRouteFromDriverApp(route(status = "ZAPLANOWANA"), "2026-06-30", today))
        assertTrue(canCompleteRouteFromDriverApp(route(status = "W TRAKCIE"), "2026-06-30", today))
        assertFalse(canCompleteRouteFromDriverApp(route(status = "ZAKOŃCZONA"), "2026-06-30", today))
        assertFalse(canCompleteRouteFromDriverApp(route(status = "NIEROZPOZNANA"), "2026-06-30", today))
        assertFalse(canCompleteRouteFromDriverApp(route(status = "ZAPLANOWANA"), "2026-07-01", today))
        assertFalse(canCompleteRouteFromDriverApp(route(status = "ZAPLANOWANA"), "2026-06-29", today))
    }

    @Test
    fun multiDayRouteCanLoadTodayButCanUnloadOnlyOnItsUnloadingDate() {
        val route = route(
            status = "ZAPLANOWANA",
            routeDate = "2026-06-29",
            unloadingDate = "2026-06-30",
        )

        assertTrue(canStartRouteFromDriverApp(route, "2026-06-30", today))
        assertEquals("Załaduj", routeLifecyclePrimaryAction(route, "2026-06-30", today)?.label)
        assertEquals("Dziś: rozładunek. Załadunek możesz potwierdzić dzisiaj.", routeTodayInstruction(route, "2026-06-30", today))

        val activeRoute = route.copy(status = "W TRAKCIE")
        assertTrue(canFinishRouteFromDriverApp(activeRoute, "2026-06-30", today))
        assertEquals("Rozładuj", routeLifecyclePrimaryAction(activeRoute, "2026-06-30", today)?.label)
        assertFalse(canFinishRouteFromDriverApp(activeRoute, "2026-06-29", LocalDate.parse("2026-06-29")))
    }

    @Test
    fun routeScheduleShowsBothDispatcherDates() {
        val route = route(routeDate = "2026-06-30", unloadingDate = "2026-07-02")

        assertEquals("Załadunek: 30.06 · Rozładunek: 02.07", routeScheduleSummary(route))
        assertEquals("Załadunek: 30.06 · Rozładunek: ten sam dzień", routeScheduleSummary(route.copy(unloadingDate = null)))
    }

    @Test
    fun showsLiveRouteDayUpdateOnlyForViewedDate() {
        assertTrue(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = "2026-03-12"))
        assertTrue(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = null))
        assertFalse(shouldShowRouteDayLiveUpdate(viewedDate = "2026-03-12", hintDate = "2026-03-13"))
        assertFalse(shouldShowRouteDayLiveUpdate(viewedDate = "", hintDate = "2026-03-12"))
    }

    @Test
    fun formatsRouteDateChipHumanFirstWithShortFallback() {
        assertEquals("dzisiaj · 30.06", routeDateChipLabel("2026-06-30", today))
        assertEquals("jutro · 01.07", routeDateChipLabel("2026-07-01", today))
        assertEquals("w poniedziałek · 06.07", routeDateChipLabel("2026-07-06", today))
        assertEquals("02.03", routeDateChipLabel("2027-03-02", today))
        assertEquals("—", routeDateChipLabel("", today))
    }

    @Test
    fun buildsNavigationCandidatesFromGeofenceCoordinatesWithFallbacks() {
        val candidates = navigationIntentCandidates(
            latitude = 50.0619474,
            longitude = 19.9368564,
            label = "Brama główna",
        )

        assertEquals(3, candidates.size)
        assertEquals("com.google.android.apps.maps", candidates[0].packageName)
        assertEquals("google.navigation:q=50.0619474,19.9368564", candidates[0].uri)
        assertEquals(null, candidates[1].packageName)
        assertEquals(
            "geo:0,0?q=50.0619474,19.9368564(Brama%20g%C5%82%C3%B3wna)",
            candidates[1].uri,
        )
        assertEquals(
            "https://www.google.com/maps/dir/?api=1&destination=50.0619474,19.9368564",
            candidates[2].uri,
        )
    }

    @Test
    fun doesNotBuildNavigationCandidatesForMissingOrInvalidCoordinates() {
        assertFalse(hasValidNavigationCoordinates(null, 19.9368564))
        assertFalse(hasValidNavigationCoordinates(50.0619474, null))
        assertFalse(hasValidNavigationCoordinates(Double.NaN, 19.9368564))
        assertFalse(hasValidNavigationCoordinates(91.0, 19.9368564))
        assertFalse(hasValidNavigationCoordinates(50.0619474, 181.0))
        assertTrue(hasValidNavigationCoordinates(50.0619474, 19.9368564))
        assertTrue(navigationIntentCandidates(null, null, "Cel").isEmpty())
    }

    @Test
    fun showsAnOptionalUpdateWhenGooglePlayOrBackendHasANewerVersion() {
        val fromPlay = appUpdateBannerUi(
            currentVersionCode = 113,
            latestVersionCode = null,
            latestVersionName = null,
            minimumSupportedVersionCode = 0,
            playUpdateAvailable = true,
            playAvailableVersionCode = 114,
            updateDownloaded = false,
        )
        val fromBackend = appUpdateBannerUi(
            currentVersionCode = 113,
            latestVersionCode = 114,
            latestVersionName = "1.0.61",
            minimumSupportedVersionCode = 0,
            playUpdateAvailable = false,
            playAvailableVersionCode = null,
            updateDownloaded = false,
        )

        assertEquals(false, fromPlay?.required)
        assertEquals("Aktualizuj", fromPlay?.actionLabel)
        assertEquals("Dostępna jest nowa wersja aplikacji 1.0.61.", fromBackend?.message)
    }

    @Test
    fun requiresAnUpdateOnlyBelowTheConfiguredMinimumVersion() {
        val required = appUpdateBannerUi(
            currentVersionCode = 111,
            latestVersionCode = 114,
            latestVersionName = "1.0.61",
            minimumSupportedVersionCode = 112,
            playUpdateAvailable = true,
            playAvailableVersionCode = 114,
            updateDownloaded = false,
        )
        val current = appUpdateBannerUi(
            currentVersionCode = 114,
            latestVersionCode = 114,
            latestVersionName = "1.0.61",
            minimumSupportedVersionCode = 112,
            playUpdateAvailable = false,
            playAvailableVersionCode = null,
            updateDownloaded = false,
        )

        assertEquals(true, required?.required)
        assertTrue(required?.message?.contains("nie jest już obsługiwana") == true)
        assertEquals(null, current)
    }

    @Test
    fun downloadedUpdateUsesInstallAction() {
        val update = appUpdateBannerUi(
            currentVersionCode = 113,
            latestVersionCode = 114,
            latestVersionName = "1.0.61",
            minimumSupportedVersionCode = 0,
            playUpdateAvailable = true,
            playAvailableVersionCode = 114,
            updateDownloaded = true,
        )

        assertEquals("Zainstaluj", update?.actionLabel)
        assertEquals(true, update?.downloaded)
        assertTrue(update?.message?.contains("gotowa do instalacji") == true)
    }

    @Test
    fun calculatesPhotoGridRowsForTwoColumnInlineGallery() {
        assertEquals(0, inlinePhotoGridRows(0))
        assertEquals(1, inlinePhotoGridRows(1))
        assertEquals(1, inlinePhotoGridRows(2))
        assertEquals(2, inlinePhotoGridRows(3))
        assertEquals(3, inlinePhotoGridRows(5))
    }

    @Test
    fun keepsPhotoPreviewFeedbackVisibleUntilImageLoadsOrFails() {
        assertTrue(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = true, isError = false))
        assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = false, isError = false))
        assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = true, isError = true))
        assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = false, isLoading = true, isError = false))

        assertTrue(shouldShowPhotoPreviewErrorState(hasPhoto = true, isError = true))
        assertFalse(shouldShowPhotoPreviewErrorState(hasPhoto = true, isError = false))
        assertFalse(shouldShowPhotoPreviewErrorState(hasPhoto = false, isError = true))
    }

    @Test
    fun keepsPhotoTileFeedbackVisibleUntilThumbnailLoadsOrFails() {
        assertTrue(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = true, isError = false))
        assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = false, isError = false))
        assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = true, isError = true))
        assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = false, isLoading = true, isError = false))

        assertTrue(shouldShowPhotoTileErrorState(hasPhoto = true, isError = true))
        assertFalse(shouldShowPhotoTileErrorState(hasPhoto = true, isError = false))
        assertFalse(shouldShowPhotoTileErrorState(hasPhoto = false, isError = true))
    }

    @Test
    fun usesShorterChromeOnCompactScreens() {
        assertEquals(118, authBrandBannerHeightDp(600))
        assertEquals(150, authBrandBannerHeightDp(690))
        assertEquals(190, authBrandBannerHeightDp(780))

        assertEquals(92, appBrandHeaderHeightDp(600))
        assertEquals(108, appBrandHeaderHeightDp(690))
        assertEquals(124, appBrandHeaderHeightDp(780))

        assertEquals(72, bottomNavigationHeightDp(600))
        assertEquals(78, bottomNavigationHeightDp(690))
        assertEquals(82, bottomNavigationHeightDp(780))
    }

    @Test
    fun reducesHorizontalSpaceOnNarrowScreens() {
        assertEquals(14, screenHorizontalPaddingDp(340))
        assertEquals(16, screenHorizontalPaddingDp(390))
        assertEquals(20, screenHorizontalPaddingDp(440))

        assertEquals(70, phonePrefixWidthDp(340))
        assertEquals(82, phonePrefixWidthDp(390))
        assertEquals(94, phonePrefixWidthDp(440))
    }

    @Test
    fun stacksDenseRouteFactsOnNarrowScreens() {
        assertEquals(1, routeFactColumns(340))
        assertEquals(2, routeFactColumns(390))
    }

    @Test
    fun countsServerPhotosAndLocalUploadsAsVisibleAttachments() {
        assertEquals(0, visiblePhotoAttachmentCount(serverPhotoCount = 0, localUploadCount = 0))
        assertEquals(1, visiblePhotoAttachmentCount(serverPhotoCount = 0, localUploadCount = 1))
        assertEquals(3, visiblePhotoAttachmentCount(serverPhotoCount = 1, localUploadCount = 2))
    }

    @Test
    fun countsOnlyUnconfirmedNonCancelledUploadsInQueue() {
        assertEquals(0, queuedPhotoUploadCount(emptyList()))
        assertEquals(
            4,
            queuedPhotoUploadCount(
                listOf(
                    "PENDING",
                    "UPLOADING",
                    "VERIFYING",
                    "FAILED_RETRYABLE",
                    "CONFIRMED",
                    "CANCELLED",
                ),
            ),
        )
        assertEquals(1, queuedPhotoUploadCount(listOf("FAILED_PERMANENT")))
    }

    @Test
    fun usesCreatedAtBeforeTakenAtForNewestFirstPhotoSorting() {
        val fallback = 100L

        assertEquals(
            1_782_892_800_000L,
            routePhotoSortEpochMillis(
                createdAt = "2026-07-01T10:00:00+02:00",
                takenAt = "2026-07-01T08:00:00+02:00",
                fallback = fallback,
            ),
        )
        assertEquals(
            1_782_885_600_000L,
            routePhotoSortEpochMillis(
                createdAt = null,
                takenAt = "2026-07-01T08:00:00+02:00",
                fallback = fallback,
            ),
        )
        assertEquals(fallback, routePhotoSortEpochMillis(createdAt = null, takenAt = null, fallback = fallback))
    }

    @Test
    fun allowsDeleteIconForConfirmedLocalUploadWithServerPhotoId() {
        assertEquals("42", confirmedUploadServerPhotoId("CONFIRMED", "42"))
        assertEquals(null, confirmedUploadServerPhotoId("CONFIRMED", ""))
        assertEquals(null, confirmedUploadServerPhotoId("UPLOADING", "42"))
        assertEquals(null, confirmedUploadServerPhotoId("FAILED_RETRYABLE", "42"))
    }

    @Test
    fun allowsRetryForRetryableAndLegacyCancelledUploads() {
        assertTrue(canRetryPhotoUpload("FAILED_RETRYABLE"))
        assertTrue(canRetryPhotoUpload("FAILED_PERMANENT", "Typ: błąd aplikacji\nKomunikat: Job was cancelled"))
        assertFalse(canRetryPhotoUpload("PENDING"))
        assertFalse(canRetryPhotoUpload("UPLOADING"))
        assertFalse(canRetryPhotoUpload("VERIFYING"))
        assertFalse(canRetryPhotoUpload("CONFIRMED"))
        assertFalse(canRetryPhotoUpload("FAILED_PERMANENT"))
        assertFalse(canRetryPhotoUpload("CANCELLED"))
    }

    @Test
    fun permanentUploadDoesNotSatisfyRouteStageAndCanBeDiscarded() {
        val failed = upload(status = "FAILED_PERMANENT")
        val pending = upload(status = "PENDING").copy(clientRequestId = "pending-upload")

        assertEquals(listOf("pending-upload"), routeStageUsableUploads(listOf(failed, pending)).map { it.clientRequestId })
        assertFalse(photoUploadCanSatisfyStage("FAILED_PERMANENT"))
        assertTrue(canDiscardLocalPhotoUpload("FAILED_PERMANENT", null))
        assertFalse(canDiscardLocalPhotoUpload("CONFIRMED", "42"))
    }

    @Test
    fun serverRefreshLeavesObsoleteRouteStageScreen() {
        assertEquals(
            DriverScreen.Detail,
            routeScreenAfterServerRefresh(DriverScreen.StartRoute, route(status = "W TRAKCIE")),
        )
        assertEquals(
            DriverScreen.Detail,
            routeScreenAfterServerRefresh(DriverScreen.LoadingWeight, route(status = "ZAKOŃCZONA")),
        )
        assertEquals(
            DriverScreen.Detail,
            routeScreenAfterServerRefresh(DriverScreen.FinishRoute, route(status = "ZAKOŃCZONA")),
        )
        assertEquals(
            DriverScreen.StartRoute,
            routeScreenAfterServerRefresh(DriverScreen.StartRoute, route(status = "ZAPLANOWANA")),
        )
    }

    @Test
    fun showsFailureDetailsOnlyForFailedUploadsWithErrorMessage() {
        assertTrue(canShowPhotoUploadFailureDetails("FAILED_RETRYABLE", "Typ: błąd serwera"))
        assertTrue(canShowPhotoUploadFailureDetails("FAILED_PERMANENT", "Typ: błąd aplikacji"))
        assertFalse(canShowPhotoUploadFailureDetails("FAILED_RETRYABLE", null))
        assertFalse(canShowPhotoUploadFailureDetails("FAILED_RETRYABLE", ""))
        assertFalse(canShowPhotoUploadFailureDetails("UPLOADING", "Typ: błąd serwera"))
        assertFalse(canShowPhotoUploadFailureDetails("CONFIRMED", "Typ: błąd serwera"))
    }

    @Test
    fun createsFullScreenPreviewForLocalUploadBeforeRouteRefresh() {
        val photo = localUploadPreviewPhoto(upload(status = "CONFIRMED", serverPhotoId = "55"))

        requireNotNull(photo)
        assertEquals("local-upload-123", photo.id)
        assertEquals("10", photo.routeId)
        assertEquals("file:/tmp/photo.jpg", photo.url)
        assertEquals("2026-07-01T08:00:00Z", photo.takenAt)
        assertEquals(50.1, photo.latitude ?: 0.0, 0.0)
        assertEquals(true, photo.canDelete)
    }

    @Test
    fun preciseLocationRequirementAllowsCameraOnlyWithFineLocation() {
        assertTrue(canLaunchCameraWithLocationPolicy(requirePreciseLocation = false, hasFineLocation = false))
        assertTrue(canLaunchCameraWithLocationPolicy(requirePreciseLocation = true, hasFineLocation = true))
        assertFalse(canLaunchCameraWithLocationPolicy(requirePreciseLocation = true, hasFineLocation = false))
    }

    @Test
    fun showsDispatchSheetCardOnlyWhenReminderIsDueToday() {
        assertTrue(shouldShowDispatchSheetReminderCard(dispatchReminder(status = "missing", dueToday = true)))
        assertTrue(shouldShowDispatchSheetReminderCard(dispatchReminder(status = "uploaded", dueToday = true)))
        assertFalse(shouldShowDispatchSheetReminderCard(dispatchReminder(status = "not_required", dueToday = false)))
        assertFalse(shouldShowDispatchSheetReminderCard(null))
    }

    @Test
    fun explainsOfflineStateWithoutCachedSync() {
        assertEquals(
            "Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.",
            offlineStaleBannerMessage(isOnline = false, isStale = false, syncLabel = "brak zapisanej synchronizacji"),
        )
    }

    @Test
    fun healthyOnlineStateDoesNotShowOfflineBanner() {
        assertEquals(
            null,
            connectionStatusBannerMessage(
                isOnline = true,
                isStale = false,
                serverConnectionState = ServerConnectionState.Reachable,
                showRealtimeReconnecting = false,
                syncLabel = "12:30",
            ),
        )
    }

    @Test
    fun serverFailureAndRealtimeReconnectAreNotCalledOffline() {
        assertEquals(
            "Internet działa, ale serwer jest chwilowo niedostępny. Pokazujemy dane z 12:30.",
            connectionStatusBannerMessage(true, true, ServerConnectionState.Degraded, false, "12:30"),
        )
        assertEquals(
            "Ponowne łączenie. Dane są uzgadniane z serwerem.",
            connectionStatusBannerMessage(true, false, ServerConnectionState.Reachable, true, "12:30"),
        )
    }

    @Test
    fun explainsStaleCachedDataWhenOnline() {
        assertEquals(
            "Dane mogą być nieaktualne. Ostatnia synchronizacja: 12:30.",
            offlineStaleBannerMessage(isOnline = true, isStale = true, syncLabel = "12:30"),
        )
    }

    @Test
    fun labelsDispatchSheetActionByStatusAndLocalUploadQueue() {
        assertEquals("Zrób zdjęcie", dispatchSheetPrimaryActionLabel(dispatchReminder(status = "missing"), hasQueuedUpload = false))
        assertEquals("Wysyłam zdjęcie...", dispatchSheetPrimaryActionLabel(dispatchReminder(status = "missing"), hasQueuedUpload = true))
        assertEquals("Popraw", dispatchSheetPrimaryActionLabel(dispatchReminder(status = "uploaded"), hasQueuedUpload = false))
    }

    @Test
    fun routeStageFormRequiresPositiveWeightAndAtLeastOnePhotoOrQueuedUpload() {
        assertTrue(canSubmitRouteStageForm(weightText = "12,5", serverPhotoCount = 1, localUploadCount = 0))
        assertTrue(canSubmitRouteStageForm(weightText = "12.5", serverPhotoCount = 0, localUploadCount = 1))
        assertTrue(canSubmitRouteStageForm(weightText = "23400", serverPhotoCount = 1, localUploadCount = 0))
        assertFalse(canSubmitRouteStageForm(weightText = "", serverPhotoCount = 1, localUploadCount = 0))
        assertFalse(canSubmitRouteStageForm(weightText = "0", serverPhotoCount = 1, localUploadCount = 0))
        assertFalse(canSubmitRouteStageForm(weightText = "12.5", serverPhotoCount = 0, localUploadCount = 0))
    }

    @Test
    fun parsesRouteWeightsEnteredInTonsOrKilograms() {
        assertEquals(23.4, parseRouteWeightTons("23,4") ?: 0.0, 0.0)
        assertEquals(23.4, parseRouteWeightTons("23400") ?: 0.0, 0.0)
        assertEquals(23.401, parseRouteWeightTons("23400,5") ?: 0.0, 0.0)
        assertEquals("Wpisano kilogramy — zapiszemy 23,400 t.", routeWeightInputHelpText("23400"))
    }

    @Test
    fun filtersRoutePhotosByStageWithOtherFallback() {
        val photos = listOf(
            photo("1", stage = "loading"),
            photo("2", stage = "unloading"),
            photo("3", stage = null),
        )

        assertEquals(listOf("1"), routePhotosForStage(photos, "loading").map { it.id })
        assertEquals(listOf("2"), routePhotosForStage(photos, "unloading").map { it.id })
        assertEquals(listOf("3"), routePhotosForStage(photos, "other").map { it.id })
    }

    @Test
    fun choosesAdaptiveGpsUploadIntervalForActiveRoute() {
        assertEquals(45, activeRouteUploadIntervalSeconds(isMoving = false))
        assertEquals(20, activeRouteUploadIntervalSeconds(isMoving = true))
        assertEquals(5, activeRouteUploadIntervalSeconds(isMoving = true, isUrbanOrTurning = true))
        assertEquals(5, activeRouteUploadIntervalSeconds(isMoving = true, isNearDestination = true))
        assertEquals(20, activeRouteUploadIntervalSeconds(isMoving = true, isHighwayStable = true))
        assertEquals(3, activeRouteUploadIntervalSeconds(isMoving = true, isLikelyOffRoute = true))
    }

    @Test
    fun notificationTapTargetsFinishScreenOnlyAfterArrival() {
        assertEquals(DriverScreen.Detail, activeRouteNotificationTarget(arrived = false))
        assertEquals(DriverScreen.FinishRoute, activeRouteNotificationTarget(arrived = true))
    }

    @Test
    fun plannedRouteShowsLoadingAsNextStepAndStartAction() {
        val route = route(status = "ZAPLANOWANA")

        val steps = routeFlowSteps(route, emptyList())
        val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)

        assertEquals(listOf("Zdjęcia załadunku", "Tonaż załadunku", "W trasie", "Rozładunek"), steps.map { it.title })
        assertEquals(RouteFlowStepState.Todo, steps[0].state)
        assertEquals(RouteFlowStepState.Todo, steps[1].state)
        assertEquals(RouteFlowStepState.Todo, steps[2].state)
        assertEquals(RouteFlowStepState.Todo, steps[3].state)
        assertEquals("Załaduj", action?.label)
        assertEquals(RouteLifecycleAction.Start, action?.action)
    }

    @Test
    fun driverFlowIgnoresOperationalStatusFromGeofence() {
        val route = route(
            status = "ZAKOŃCZONA",
            driverStatus = "ZAPLANOWANA",
            operationalStatus = "ZAKOŃCZONA",
        )

        val steps = routeFlowSteps(route, emptyList())

        assertTrue(canStartRouteFromDriverApp(route, "2026-06-30", today))
        assertEquals(RouteFlowStepState.Todo, steps[0].state)
        assertEquals(RouteFlowStepState.Todo, steps[1].state)
        assertEquals(RouteFlowStepState.Todo, steps[2].state)
        assertEquals(DriverScreen.StartRoute, routeScreenAfterServerRefresh(DriverScreen.StartRoute, route))
    }

    @Test
    fun plannedRouteWithLoadingPhotoShowsOnlyPhotoStepComplete() {
        val route = route(status = "ZAPLANOWANA", photos = listOf(photo("loading-1", stage = "loading")))

        val steps = routeFlowSteps(route, emptyList())

        assertEquals(RouteFlowStepState.ServerSaved, steps[0].state)
        assertEquals("zapisane na serwerze", steps[0].stateLabel)
        assertEquals(RouteFlowStepState.Todo, steps[1].state)
        assertEquals(RouteFlowStepState.Todo, steps[2].state)
        assertEquals(RouteFlowStepState.Todo, steps[3].state)
    }

    @Test
    fun freshInstallLabelsBackendLoadingDocumentationAsServerData() {
        val route = route(
            status = "ZAPLANOWANA",
            loadingWeight = 23.76,
            photos = listOf(photo("loading-1", stage = "loading")),
        )

        val steps = routeFlowSteps(route, emptyList())

        assertEquals(RouteFlowStepState.ServerSaved, steps[0].state)
        assertEquals("zapisane na serwerze", steps[0].stateLabel)
        assertEquals(RouteFlowStepState.ServerSaved, steps[1].state)
        assertEquals("zapisane na serwerze", steps[1].stateLabel)
        assertEquals(RouteFlowStepState.Todo, steps[2].state)
        assertEquals(RouteFlowStepState.Todo, steps[3].state)
    }

    @Test
    fun serverWeightDoesNotMarkMissingLoadingPhotoAsComplete() {
        val steps = routeFlowSteps(route(status = "ZAPLANOWANA", loadingWeight = 23.76), emptyList())

        assertEquals(RouteFlowStepState.Todo, steps[0].state)
        assertEquals(RouteFlowStepState.ServerSaved, steps[1].state)
    }

    @Test
    fun activeRouteShowsTransitStateAndFinishAction() {
        val route = route(status = "W TRAKCIE", loadingWeight = 12.5)

        val steps = routeFlowSteps(route, emptyList())
        val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)

        assertEquals(RouteFlowStepState.Confirmed, steps[0].state)
        assertEquals(RouteFlowStepState.Confirmed, steps[1].state)
        assertEquals(RouteFlowStepState.InProgress, steps[2].state)
        assertEquals("w trakcie", steps[2].stateLabel)
        assertEquals(RouteFlowStepState.Todo, steps[3].state)
        assertEquals("Rozładuj", action?.label)
        assertEquals(RouteLifecycleAction.Finish, action?.action)
    }

    @Test
    fun finishedRouteShowsCompletedStagesAndNoPrimaryAction() {
        val route = route(status = "ZAKOŃCZONA", loadingWeight = 12.5, unloadingWeight = 11.8)

        val steps = routeFlowSteps(route, emptyList())
        val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)

        assertEquals(RouteFlowStepState.Confirmed, steps[0].state)
        assertEquals(RouteFlowStepState.Confirmed, steps[1].state)
        assertEquals(RouteFlowStepState.Confirmed, steps[2].state)
        assertEquals(RouteFlowStepState.Confirmed, steps[3].state)
        assertEquals(null, action)
    }

    @Test
    fun pendingLocalRouteActionShowsSyncingCallout() {
        val actions = listOf(action(RouteActionType.Start, RouteActionStatus.WaitingForPhotos))

        val steps = routeFlowSteps(route(status = "W TRAKCIE", loadingWeight = 12.5), actions)
        val callout = routeSyncCallout(actions)

        assertEquals(RouteFlowStepState.Syncing, steps[1].state)
        assertEquals("Czekam na wysłanie zdjęć etapu, potem wyślę zmianę statusu.", callout?.text)
        assertFalse(callout?.isConflict ?: true)
    }

    @Test
    fun failedConflictRouteActionRequiresAttention() {
        val actions = listOf(action(RouteActionType.Finish, RouteActionStatus.FailedConflict, "Kurs został przepisany."))

        val steps = routeFlowSteps(route(status = "W TRAKCIE", loadingWeight = 12.5), actions)
        val callout = routeSyncCallout(actions)

        assertEquals(RouteFlowStepState.NeedsAttention, steps[3].state)
        assertEquals("Kurs wymaga obsługi. Dane wpisane w telefonie zostały zachowane.", callout?.text)
        assertTrue(callout?.isConflict ?: false)
        assertFalse(callout?.canCorrect ?: true)
    }

    @Test
    fun permanentlyRejectedRouteActionOffersDataCorrection() {
        val callout = routeSyncCallout(
            listOf(action(RouteActionType.Start, RouteActionStatus.FailedPermanent, "Brakuje wymaganych danych.")),
        )

        assertTrue(callout?.canCorrect ?: false)
    }

    @Test
    fun routeStageSubmitBlockerExplainsMissingRequirements() {
        assertEquals("Podaj tonaż.", routeStageSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
        assertEquals("Tonaż musi być większy od zera.", routeStageSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
        assertEquals("Dodaj co najmniej jedno zdjęcie etapu.", routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 0))
        assertEquals(null, routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 1))
    }

    @Test
    fun loadingPhotosStepRequiresAtLeastOneLoadingPhotoBeforeWeightStep() {
        assertEquals(
            "Dodaj co najmniej jedno zdjęcie załadunku.",
            loadingPhotosSubmitBlocker(serverPhotoCount = 0, localUploadCount = 0),
        )
        assertEquals(null, loadingPhotosSubmitBlocker(serverPhotoCount = 1, localUploadCount = 0))
        assertEquals(null, loadingPhotosSubmitBlocker(serverPhotoCount = 0, localUploadCount = 1))
    }

    @Test
    fun loadingWeightStepRequiresPositiveWeightAndPreviousLoadingPhoto() {
        assertEquals("Podaj tonaż.", loadingWeightSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
        assertEquals("Podaj poprawny tonaż.", loadingWeightSubmitBlocker("abc", serverPhotoCount = 1, localUploadCount = 0))
        assertEquals("Tonaż musi być większy od zera.", loadingWeightSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
        assertEquals(
            "Dodaj co najmniej jedno zdjęcie załadunku.",
            loadingWeightSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 0),
        )
        assertEquals(null, loadingWeightSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 1))
    }

    @Test
    fun optionalStageInputsAreVisibleButDoNotBlockConfirmation() {
        assertEquals(
            null,
            routeStageSubmitBlocker(
                weightText = "",
                serverPhotoCount = 0,
                localUploadCount = 0,
                weightRequirement = RouteStageRequirement.Optional,
                photoRequirement = RouteStageRequirement.Optional,
            ),
        )
        assertEquals(
            "Podaj poprawny tonaż.",
            routeStageSubmitBlocker(
                weightText = "abc",
                serverPhotoCount = 0,
                localUploadCount = 0,
                weightRequirement = RouteStageRequirement.Optional,
                photoRequirement = RouteStageRequirement.Optional,
            ),
        )
    }

    @Test
    fun disabledStageInputsAreHiddenAndDoNotBlockConfirmation() {
        assertEquals(RouteStageRequirement.Required, normalizeRouteStageRequirement(null))
        assertEquals(RouteStageRequirement.Required, normalizeRouteStageRequirement("unknown"))
        assertFalse(routeStageRequirementIsVisible(RouteStageRequirement.Disabled))
        assertTrue(routeStageRequirementIsVisible(RouteStageRequirement.Optional))
        assertTrue(routeStageRequirementIsRequired(RouteStageRequirement.Required))
        assertEquals(
            null,
            routeStageSubmitBlocker(
                weightText = "invalid value is ignored",
                serverPhotoCount = 0,
                localUploadCount = 0,
                weightRequirement = RouteStageRequirement.Disabled,
                photoRequirement = RouteStageRequirement.Disabled,
            ),
        )
    }

    @Test
    fun disabledLoadingStepsAreRemovedFromRouteFlow() {
        val steps = routeFlowSteps(
            route(status = "ZAPLANOWANA"),
            emptyList(),
            loadingPhotoRequirement = RouteStageRequirement.Disabled,
            loadingWeightRequirement = RouteStageRequirement.Disabled,
        )

        assertEquals(listOf("W trasie", "Rozładunek"), steps.map { it.title })
    }

    private fun route(
        status: String = "ZAPLANOWANA",
        driverStatus: String? = null,
        operationalStatus: String? = null,
        loadingWeight: Double? = null,
        unloadingWeight: Double? = null,
        photos: List<RoutePhotoDto> = emptyList(),
        routeDate: String? = "2026-06-30",
        unloadingDate: String? = null,
    ): DriverRouteDto =
        DriverRouteDto(
            id = "1",
            routeDate = routeDate,
            unloadingDate = unloadingDate,
            startsAt = "",
            originName = "Baza",
            destinationName = "Instalacja",
            contractorName = "TPP",
            contractName = "Kontrakt",
            contractCode = "TPP-1",
            relationLabel = "Baza -> Instalacja",
            status = status,
            driverStatus = driverStatus,
            operationalStatus = operationalStatus,
            loadingWeight = loadingWeight,
            unloadingWeight = unloadingWeight,
            distanceKm = 1.0,
            notes = null,
            truck = null,
            photos = photos,
        )

    private fun action(type: String, status: String, lastError: String? = null): RouteActionEntity =
        RouteActionEntity(
            clientActionId = "action-$type-$status",
            routeId = "1",
            action = type,
            weight = 12.5,
            occurredAt = "2026-06-30T07:40:00Z",
            photoClientRequestIdsJson = "[]",
            status = status,
            lastError = lastError,
        )

    private fun upload(status: String, serverPhotoId: String? = null): PhotoUploadEntity =
        PhotoUploadEntity(
            clientRequestId = "local-upload-123",
            routeId = "10",
            localPath = "/tmp/photo.jpg",
            source = "camera",
            takenAt = "2026-07-01T08:00:00Z",
            latitude = 50.1,
            longitude = 19.9,
            locationAccuracyMeters = 8.0,
            mimeType = "image/jpeg",
            size = 1200,
            contentSha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            status = status,
            serverPhotoId = serverPhotoId,
        )

    private fun photo(id: String, stage: String?): RoutePhotoDto =
        RoutePhotoDto(
            id = id,
            routeId = "10",
            clientRequestId = null,
            contentSha256 = null,
            source = "camera",
            stage = stage ?: "other",
            mimeType = "image/jpeg",
            size = 1200,
            url = "https://example.test/$id.jpg",
            takenAt = null,
            latitude = null,
            longitude = null,
            locationAccuracyMeters = null,
            createdAt = null,
        )

    private fun dispatchReminder(status: String, dueToday: Boolean = true): DispatchSheetReminderDto =
        DispatchSheetReminderDto(
            enabled = dueToday,
            dueToday = dueToday,
            workDate = "2026-06-30",
            reason = if (dueToday) "friday" else null,
            availableUntil = "2026-06-30T23:59:59+02:00",
            status = status,
            photo = null,
            canUpload = dueToday,
        )
}
