package de.krisenvorrat.app.data.sync

import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
import de.krisenvorrat.app.domain.model.SyncError
import de.krisenvorrat.app.domain.repository.SettingsRepository
import de.krisenvorrat.shared.model.CategoryDto
import de.krisenvorrat.shared.model.InventoryDto
import de.krisenvorrat.shared.model.ItemDto
import de.krisenvorrat.shared.model.LocationDto
import de.krisenvorrat.shared.model.SettingDto
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.client.engine.mock.respondError
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import io.ktor.serialization.kotlinx.json.json
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class SyncServiceImplTest {

    private val settingsRepository: SettingsRepository = mockk()
    private val jsonSerializer = Json {
        ignoreUnknownKeys = true
        encodeDefaults = true
    }

    private val testInventory = InventoryDto(
        categories = listOf(CategoryDto(id = 1, name = "Konserven")),
        locations = listOf(LocationDto(id = 1, name = "Keller")),
        items = listOf(
            ItemDto(
                id = "item-1",
                name = "Bohnen",
                categoryId = 1,
                quantity = 5.0,
                unit = "Dose",
                unitPrice = 1.29,
                kcalPerKg = 100,
                expiryDate = "2027-06-01",
                locationId = 1,
                notes = "",
                lastUpdated = System.currentTimeMillis()
            )
        ),
        settings = listOf(SettingDto(key = "household_size", value = "2"))
    )

    private lateinit var httpClient: HttpClient
    private lateinit var syncService: SyncServiceImpl

    @After
    fun tearDown() {
        if (::httpClient.isInitialized) {
            httpClient.close()
        }
    }

    private fun createClient(engine: MockEngine): HttpClient = HttpClient(engine) {
        install(ContentNegotiation) {
            json(jsonSerializer)
        }
    }

    private fun setupSettings(serverUrl: String? = "http://localhost:8080", accessToken: String? = "test-token") {
        coEvery { settingsRepository.getString(StringKey.ServerUrl) } returns (serverUrl ?: "")
        coEvery { settingsRepository.getString(StringKey.AuthAccessToken) } returns (accessToken ?: "")
        coEvery { settingsRepository.getString(StringKey.AuthRefreshToken) } returns ""
    }

    // --- downloadInventory ---

    @Test
    fun test_downloadInventory_serverReturnsInventory_returnsSuccess() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine { request ->
            assertEquals("/api/inventory", request.url.encodedPath)
            assertEquals(HttpMethod.Get, request.method)
            assertEquals("Bearer test-token", request.headers["Authorization"])
            respond(
                content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isSuccess)
        assertEquals(testInventory, result.getOrNull())
    }

    @Test
    fun test_downloadInventory_serverReturns401_returnsAuthError() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine {
            respondError(HttpStatusCode.Unauthorized)
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isFailure)
        assertTrue(result.exceptionOrNull() is SyncError.AuthError)
    }

    @Test
    fun test_downloadInventory_serverReturns500_returnsServerError() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine {
            respondError(HttpStatusCode.InternalServerError)
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isFailure)
        val error = result.exceptionOrNull() as SyncError.ServerError
        assertEquals(500, error.statusCode)
    }

    @Test
    fun test_downloadInventory_noServerUrl_returnsNotConfigured() = runTest {
        // Given
        setupSettings(serverUrl = null)
        val engine = MockEngine { respondError(HttpStatusCode.OK) }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isFailure)
        assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
    }

    @Test
    fun test_downloadInventory_emptyServerUrl_returnsNotConfigured() = runTest {
        // Given
        setupSettings(serverUrl = "  ")
        val engine = MockEngine { respondError(HttpStatusCode.OK) }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isFailure)
        assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
    }

    @Test
    fun test_downloadInventory_noAccessToken_returnsNotConfigured() = runTest {
        // Given
        setupSettings(accessToken = null)
        val engine = MockEngine { respondError(HttpStatusCode.OK) }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory()

        // Then
        assertTrue(result.isFailure)
        assertTrue(result.exceptionOrNull() is SyncError.NotConfigured)
    }

    @Test
    fun test_downloadInventory_withSince_appendsQueryParam() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine { request ->
            assertEquals("/api/inventory", request.url.encodedPath)
            assertEquals("1715000000", request.url.parameters["since"])
            respond(
                content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory(since = 1715000000L)

        // Then
        assertTrue(result.isSuccess)
    }

    @Test
    fun test_downloadInventory_withoutSince_noQueryParam() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine { request ->
            assertTrue(request.url.parameters.isEmpty())
            respond(
                content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.downloadInventory(since = null)

        // Then
        assertTrue(result.isSuccess)
    }

    // --- uploadInventory ---

    @Test
    fun test_uploadInventory_serverAccepts_returnsSuccess() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine { request ->
            assertEquals("/api/inventory", request.url.encodedPath)
            assertEquals(HttpMethod.Put, request.method)
            assertEquals("Bearer test-token", request.headers["Authorization"])
            assertEquals(
                ContentType.Application.Json.toString(),
                request.body.contentType.toString()
            )
            respond(
                content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.uploadInventory(testInventory)

        // Then
        assertTrue(result.isSuccess)
        assertEquals(testInventory, result.getOrNull())
    }

    @Test
    fun test_uploadInventory_serverReturns401_returnsAuthError() = runTest {
        // Given
        setupSettings()
        val engine = MockEngine {
            respondError(HttpStatusCode.Unauthorized)
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.uploadInventory(testInventory)

        // Then
        assertTrue(result.isFailure)
        assertTrue(result.exceptionOrNull() is SyncError.AuthError)
    }

    @Test
    fun test_uploadInventory_serverUrlHasTrailingSlash_urlIsCorrect() = runTest {
        // Given
        setupSettings(serverUrl = "http://localhost:8080/")
        val engine = MockEngine { request ->
            assertEquals("http://localhost:8080/api/inventory", request.url.toString())
            respond(
                content = jsonSerializer.encodeToString(InventoryDto.serializer(), testInventory),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }
        httpClient = createClient(engine)
        syncService = SyncServiceImpl(httpClient, settingsRepository)

        // When
        val result = syncService.uploadInventory(testInventory)

        // Then
        assertTrue(result.isSuccess)
    }
}
