package dev.adriankuta.flights.model.repository.usecases

import dev.adriankuta.flights.domain.types.AirportInfo
import dev.adriankuta.flights.domain.types.City
import dev.adriankuta.flights.domain.types.Coordinates
import dev.adriankuta.flights.domain.types.Country
import dev.adriankuta.flights.domain.types.MacCity
import dev.adriankuta.flights.domain.types.Region
import dev.adriankuta.flights.model.datasource.airports.AirportsDatasource
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
import dev.adriankuta.flights.model.datasource.shared.Cache
import dev.adriankuta.model.data.api.AirportService
import dev.adriankuta.model.data.api.entities.AirportResponse
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class ObserveAirportsUseCaseImplTest {

    private lateinit var airportService: AirportService
    private lateinit var airportsDatasource: AirportsDatasource
    private lateinit var useCase: ObserveAirportsUseCaseImpl
    private lateinit var airportsFlow: MutableStateFlow<Cache<List<AirportInfoModel>>>

    @Before
    fun setup() {
        airportService = mockk()
        airportsDatasource = mockk()
        airportsFlow = MutableStateFlow(TestCache(null, null))

        every { airportsDatasource.airports } returns airportsFlow

        useCase = ObserveAirportsUseCaseImpl(
            airportService = airportService,
            airportsDatasource = airportsDatasource,
        )
    }

    @Test
    @Suppress("LongMethod")
    fun `invoke returns mapped airports when datasource returns valid data`() = runTest {
        // Given
        val airportInfoModel1 = TestAirportInfoModel(
            code = "WAW",
            name = "Warsaw Chopin",
            seoName = "Warsaw",
            isBase = true,
            timeZone = "Europe/Warsaw",
            cityCode = "WAW",
            cityName = "Warsaw",
            macCode = "WAW",
            regionCode = "PL-MZ",
            regionName = "Mazovia",
            countryCode = "PL",
            countryName = "Poland",
            countryCurrencyCode = "PLN",
            latitude = 52.1657,
            longitude = 20.9671,
        )

        val airportInfoModel2 = TestAirportInfoModel(
            code = "LDN",
            name = "London Heathrow",
            seoName = "London",
            isBase = true,
            timeZone = "Europe/London",
            cityCode = "LDN",
            cityName = "London",
            macCode = "LDN",
            regionCode = "GB-LDN",
            regionName = "London",
            countryCode = "GB",
            countryName = "United Kingdom",
            countryCurrencyCode = "GBP",
            latitude = 51.4700,
            longitude = -0.4543,
        )

        AirportResponse(
            code = "WAW",
            name = "Warsaw Chopin",
            seoName = "Warsaw",
            isBase = true,
            timeZone = "Europe/Warsaw",
            city = AirportResponse.City(
                code = "WAW",
                name = "Warsaw",
            ),
            macCity = AirportResponse.MacCity(
                code = "WAW",
                macCode = "WAW",
                name = "Warsaw",
            ),
            region = AirportResponse.Region(
                code = "PL-MZ",
                name = "Mazovia",
            ),
            country = AirportResponse.Country(
                code = "PL",
                name = "Poland",
                currencyCode = "PLN",
            ),
            coordinates = AirportResponse.Coordinates(
                latitude = 52.1657,
                longitude = 20.9671,
            ),
        )

        airportsFlow.value = TestCache("test-key", listOf(airportInfoModel1, airportInfoModel2))

        // When
        val result = useCase.invoke().first()

        // Then
        val expectedAirports = listOf(
            AirportInfo(
                code = "WAW",
                name = "Warsaw Chopin",
                seoName = "Warsaw",
                isBase = true,
                timeZone = "Europe/Warsaw",
                city = City(
                    code = "WAW",
                    name = "Warsaw",
                ),
                macCity = MacCity(
                    macCode = "WAW",
                ),
                region = Region(
                    code = "PL-MZ",
                    name = "Mazovia",
                ),
                country = Country(
                    code = "PL",
                    name = "Poland",
                    currencyCode = "PLN",
                ),
                coordinates = Coordinates(
                    latitude = 52.1657,
                    longitude = 20.9671,
                ),
            ),
            AirportInfo(
                code = "LDN",
                name = "London Heathrow",
                seoName = "London",
                isBase = true,
                timeZone = "Europe/London",
                city = City(
                    code = "LDN",
                    name = "London",
                ),
                macCity = MacCity(
                    macCode = "LDN",
                ),
                region = Region(
                    code = "GB-LDN",
                    name = "London",
                ),
                country = Country(
                    code = "GB",
                    name = "United Kingdom",
                    currencyCode = "GBP",
                ),
                coordinates = Coordinates(
                    latitude = 51.4700,
                    longitude = -0.4543,
                ),
            ),
        )

        assertEquals(expectedAirports, result)
    }

    @Test
    fun `invoke returns empty list when datasource returns empty list`() = runTest {
        // Given
        airportsFlow.value = TestCache("test-key", emptyList())

        // When
        val result = useCase.invoke().first()

        // Then
        assertEquals(emptyList<AirportInfo>(), result)
    }

    @Test
    fun `invoke returns empty list when datasource returns null`() = runTest {
        // Given
        airportsFlow.value = TestCache("test-key", null)

        // When
        val result = useCase.invoke().first()

        // Then
        assertEquals(emptyList<AirportInfo>(), result)
    }

    @Test
    @Suppress("LongMethod")
    fun `invoke triggers cache refresh when cache key is null`() = runTest {
        // Given
        val airportResponse = listOf(
            AirportResponse(
                code = "WAW",
                name = "Warsaw Chopin",
                seoName = "Warsaw",
                isBase = true,
                timeZone = "Europe/Warsaw",
                city = AirportResponse.City(
                    code = "WAW",
                    name = "Warsaw",
                ),
                macCity = AirportResponse.MacCity(
                    code = "WAW",
                    macCode = "WAW",
                    name = "Warsaw",
                ),
                region = AirportResponse.Region(
                    code = "PL-MZ",
                    name = "Mazovia",
                ),
                country = AirportResponse.Country(
                    code = "PL",
                    name = "Poland",
                    currencyCode = "PLN",
                ),
                coordinates = AirportResponse.Coordinates(
                    latitude = 52.1657,
                    longitude = 20.9671,
                ),
            ),
        )

        // Create a test model that will be returned after cache refresh
        val testModel = TestAirportInfoModel(
            code = "WAW",
            name = "Warsaw Chopin",
            seoName = "Warsaw",
            isBase = true,
            timeZone = "Europe/Warsaw",
            cityCode = "WAW",
            cityName = "Warsaw",
            macCode = "WAW",
            regionCode = "PL-MZ",
            regionName = "Mazovia",
            countryCode = "PL",
            countryName = "Poland",
            countryCurrencyCode = "PLN",
            latitude = 52.1657,
            longitude = 20.9671,
        )

        // Start with null cache key to trigger refresh
        airportsFlow.value = TestCache(null, null)

        // Set up mocks
        coEvery { airportService.getAirports("pl") } returns airportResponse
        coEvery { airportsDatasource.setAirportsInfo(any(), any()) } answers {
            // Update the flow with new data when setAirportsInfo is called
            airportsFlow.value = TestCache("new-cache-key", listOf(testModel))
        }

        // Create the expected result
        val expectedAirport = AirportInfo(
            code = "WAW",
            name = "Warsaw Chopin",
            seoName = "Warsaw",
            isBase = true,
            timeZone = "Europe/Warsaw",
            city = City(
                code = "WAW",
                name = "Warsaw",
            ),
            macCity = MacCity(
                macCode = "WAW",
            ),
            region = Region(
                code = "PL-MZ",
                name = "Mazovia",
            ),
            country = Country(
                code = "PL",
                name = "Poland",
                currencyCode = "PLN",
            ),
            coordinates = Coordinates(
                latitude = 52.1657,
                longitude = 20.9671,
            ),
        )

        // When - collect from the flow and wait for the first emission
        val flow = useCase.invoke()

        // Manually update the flow to simulate cache refresh
        // This ensures the flow has a value before we collect it
        airportsFlow.value = TestCache("new-cache-key", listOf(testModel))

        // Now collect the first value
        val result = flow.first()

        // Then
        assertEquals(listOf(expectedAirport), result)
    }

    private data class TestCache<T>(
        override val cacheKey: String?,
        override val data: T?,
    ) : Cache<T>

    private data class TestAirportInfoModel(
        override val code: String,
        override val name: String,
        override val seoName: String,
        override val isBase: Boolean,
        override val timeZone: String,
        override val cityCode: String,
        override val cityName: String,
        override val macCode: String,
        override val regionCode: String,
        override val regionName: String,
        override val countryCode: String,
        override val countryName: String,
        override val countryCurrencyCode: String,
        override val latitude: Double,
        override val longitude: Double,
    ) : AirportInfoModel
}
