package com.atridad.ascently

import com.atridad.ascently.data.format.*
import com.atridad.ascently.data.model.*
import org.junit.Assert.*
import org.junit.Test

class SyncMergeLogicTest {

    @Test
    fun `test intelligent merge preserves all data`() {
        // Create local data
        val localGyms =
            listOf(
                BackupGym(
                    id = "gym1",
                    name = "Local Gym 1",
                    location = "Local Location",
                    supportedClimbTypes = listOf(ClimbType.BOULDER),
                    difficultySystems = listOf(DifficultySystem.V_SCALE),
                    customDifficultyGrades = emptyList(),
                    notes = null,
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T10:00:00",
                ),
            )

        val localProblems =
            listOf(
                BackupProblem(
                    id = "problem1",
                    gymId = "gym1",
                    name = "Local Problem",
                    description = "Local description",
                    climbType = ClimbType.BOULDER,
                    difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
                    tags = listOf("local"),
                    location = null,
                    imagePaths = listOf("local_image.jpg"),
                    isActive = true,
                    dateSet = null,
                    notes = null,
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T10:00:00",
                ),
            )

        val localSessions =
            listOf(
                BackupClimbSession(
                    id = "session1",
                    gymId = "gym1",
                    date = "2024-01-01",
                    startTime = "2024-01-01T10:00:00",
                    endTime = "2024-01-01T12:00:00",
                    duration = 7200,
                    status = SessionStatus.COMPLETED,
                    notes = null,
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T10:00:00",
                ),
            )

        val localAttempts =
            listOf(
                BackupAttempt(
                    id = "attempt1",
                    sessionId = "session1",
                    problemId = "problem1",
                    result = AttemptResult.SUCCESS,
                    highestHold = null,
                    notes = null,
                    duration = 300,
                    restTime = null,
                    timestamp = "2024-01-01T10:30:00",
                    createdAt = "2024-01-01T10:30:00",
                    updatedAt = "2024-01-01T10:30:00",
                ),
            )

        val localBackup =
            ClimbDataBackup(
                exportedAt = "2024-01-01T10:00:00",
                version = "2.0",
                formatVersion = "2.0",
                gyms = localGyms,
                problems = localProblems,
                sessions = localSessions,
                attempts = localAttempts,
            )

        // Create server data with some overlapping and some unique data
        val serverGyms =
            listOf(
                // Same gym but with newer update
                BackupGym(
                    id = "gym1",
                    name = "Updated Gym 1",
                    location = "Updated Location",
                    supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
                    difficultySystems =
                    listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
                    customDifficultyGrades = emptyList(),
                    notes = "Updated notes",
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T12:00:00", // Newer update
                ),
                // Unique server gym
                BackupGym(
                    id = "gym2",
                    name = "Server Gym 2",
                    location = "Server Location",
                    supportedClimbTypes = listOf(ClimbType.ROPE),
                    difficultySystems = listOf(DifficultySystem.YDS),
                    customDifficultyGrades = emptyList(),
                    notes = null,
                    createdAt = "2024-01-01T11:00:00",
                    updatedAt = "2024-01-01T11:00:00",
                ),
            )

        val serverProblems =
            listOf(
                // Same problem but with newer update and different images
                BackupProblem(
                    id = "problem1",
                    gymId = "gym1",
                    name = "Updated Problem",
                    description = "Updated description",
                    climbType = ClimbType.BOULDER,
                    difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
                    tags = listOf("updated", "server"),
                    location = "Updated location",
                    imagePaths = listOf("server_image.jpg"),
                    isActive = true,
                    dateSet = "2024-01-01",
                    notes = "Updated notes",
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T11:00:00", // Newer update
                ),
                // Unique server problem
                BackupProblem(
                    id = "problem2",
                    gymId = "gym2",
                    name = "Server Problem",
                    description = "Server description",
                    climbType = ClimbType.ROPE,
                    difficulty = DifficultyGrade(DifficultySystem.YDS, "5.10a"),
                    tags = listOf("server"),
                    location = null,
                    imagePaths = null,
                    isActive = true,
                    dateSet = null,
                    notes = null,
                    createdAt = "2024-01-01T11:00:00",
                    updatedAt = "2024-01-01T11:00:00",
                ),
            )

        val serverSessions =
            listOf(
                // Unique server session
                BackupClimbSession(
                    id = "session2",
                    gymId = "gym2",
                    date = "2024-01-02",
                    startTime = "2024-01-02T14:00:00",
                    endTime = "2024-01-02T16:00:00",
                    duration = 7200,
                    status = SessionStatus.COMPLETED,
                    notes = "Server session",
                    createdAt = "2024-01-02T14:00:00",
                    updatedAt = "2024-01-02T14:00:00",
                ),
            )

        val serverAttempts =
            listOf(
                // Unique server attempt
                BackupAttempt(
                    id = "attempt2",
                    sessionId = "session2",
                    problemId = "problem2",
                    result = AttemptResult.FALL,
                    highestHold = "Last move",
                    notes = "Almost had it",
                    duration = 180,
                    restTime = 60,
                    timestamp = "2024-01-02T14:30:00",
                    createdAt = "2024-01-02T14:30:00",
                    updatedAt = "2024-01-02T14:30:00",
                ),
            )

        val serverBackup =
            ClimbDataBackup(
                exportedAt = "2024-01-01T12:00:00",
                version = "2.0",
                formatVersion = "2.0",
                gyms = serverGyms,
                problems = serverProblems,
                sessions = serverSessions,
                attempts = serverAttempts,
            )

        // Simulate merge logic
        val mergedBackup = performIntelligentMerge(localBackup, serverBackup)

        // Verify merge results
        assertEquals("Should have 2 gyms (1 updated, 1 new)", 2, mergedBackup.gyms.size)
        assertEquals("Should have 2 problems (1 updated, 1 new)", 2, mergedBackup.problems.size)
        assertEquals("Should have 2 sessions (1 local, 1 server)", 2, mergedBackup.sessions.size)
        assertEquals("Should have 2 attempts (1 local, 1 server)", 2, mergedBackup.attempts.size)

        // Verify gym merge - server version should win (newer update)
        val mergedGym1 = mergedBackup.gyms.find { it.id == "gym1" }!!
        assertEquals("Updated Gym 1", mergedGym1.name)
        assertEquals("Updated Location", mergedGym1.location)
        assertEquals("Updated notes", mergedGym1.notes)
        assertEquals("2024-01-01T12:00:00", mergedGym1.updatedAt)

        // Verify unique server gym is preserved
        val mergedGym2 = mergedBackup.gyms.find { it.id == "gym2" }!!
        assertEquals("Server Gym 2", mergedGym2.name)

        // Verify problem merge - server version should win but images should be merged
        val mergedProblem1 = mergedBackup.problems.find { it.id == "problem1" }!!
        assertEquals("Updated Problem", mergedProblem1.name)
        assertEquals("Updated description", mergedProblem1.description)
        assertEquals("2024-01-01T11:00:00", mergedProblem1.updatedAt)

        // Images should be merged (both local and server images preserved)
        assertTrue(
            "Should contain local image",
            mergedProblem1.imagePaths!!.contains("local_image.jpg"),
        )
        assertTrue(
            "Should contain server image",
            mergedProblem1.imagePaths!!.contains("server_image.jpg"),
        )
        assertEquals("Should have 2 images total", 2, mergedProblem1.imagePaths!!.size)

        // Verify unique server problem is preserved
        val mergedProblem2 = mergedBackup.problems.find { it.id == "problem2" }!!
        assertEquals("Server Problem", mergedProblem2.name)

        // Verify all sessions are preserved
        assertTrue(
            "Should contain local session",
            mergedBackup.sessions.any { it.id == "session1" },
        )
        assertTrue(
            "Should contain server session",
            mergedBackup.sessions.any { it.id == "session2" },
        )

        // Verify all attempts are preserved
        assertTrue(
            "Should contain local attempt",
            mergedBackup.attempts.any { it.id == "attempt1" },
        )
        assertTrue(
            "Should contain server attempt",
            mergedBackup.attempts.any { it.id == "attempt2" },
        )
    }

    @Test
    fun `test date comparison logic`() {
        assertTrue(
            "ISO instant should be newer",
            isNewerThan("2024-01-01T12:00:00Z", "2024-01-01T10:00:00Z"),
        )
        assertFalse(
            "ISO instant should be older",
            isNewerThan("2024-01-01T10:00:00Z", "2024-01-01T12:00:00Z"),
        )
        assertTrue(
            "String comparison should work as fallback",
            isNewerThan("2024-01-02T10:00:00", "2024-01-01T10:00:00"),
        )
    }

    @Test
    fun `test empty data scenarios`() {
        val emptyBackup =
            ClimbDataBackup(
                exportedAt = "2024-01-01T10:00:00",
                version = "2.0",
                formatVersion = "2.0",
                gyms = emptyList(),
                problems = emptyList(),
                sessions = emptyList(),
                attempts = emptyList(),
            )

        val dataBackup =
            ClimbDataBackup(
                exportedAt = "2024-01-01T10:00:00",
                version = "2.0",
                formatVersion = "2.0",
                gyms =
                listOf(
                    BackupGym(
                        id = "gym1",
                        name = "Test Gym",
                        location = null,
                        supportedClimbTypes = listOf(ClimbType.BOULDER),
                        difficultySystems =
                        listOf(DifficultySystem.V_SCALE),
                        customDifficultyGrades = emptyList(),
                        notes = null,
                        createdAt = "2024-01-01T10:00:00",
                        updatedAt = "2024-01-01T10:00:00",
                    ),
                ),
                problems = emptyList(),
                sessions = emptyList(),
                attempts = emptyList(),
            )

        // Test merging empty with data
        val merged1 = performIntelligentMerge(emptyBackup, dataBackup)
        assertEquals("Should preserve data from non-empty backup", 1, merged1.gyms.size)

        // Test merging data with empty
        val merged2 = performIntelligentMerge(dataBackup, emptyBackup)
        assertEquals("Should preserve data from non-empty backup", 1, merged2.gyms.size)

        // Test merging empty with empty
        val merged3 = performIntelligentMerge(emptyBackup, emptyBackup)
        assertEquals("Should remain empty", 0, merged3.gyms.size)
    }

    // Helper methods that simulate the merge logic from SyncService
    private fun performIntelligentMerge(
        local: ClimbDataBackup,
        server: ClimbDataBackup,
    ): ClimbDataBackup {
        val mergedGyms = mergeGyms(local.gyms, server.gyms)
        val mergedProblems = mergeProblems(local.problems, server.problems)
        val mergedSessions = mergeSessions(local.sessions, server.sessions)
        val mergedAttempts = mergeAttempts(local.attempts, server.attempts)

        return ClimbDataBackup(
            exportedAt = "2024-01-01T12:00:00",
            version = "2.0",
            formatVersion = "2.0",
            gyms = mergedGyms,
            problems = mergedProblems,
            sessions = mergedSessions,
            attempts = mergedAttempts,
        )
    }

    private fun mergeGyms(local: List<BackupGym>, server: List<BackupGym>): List<BackupGym> {
        val merged = mutableMapOf<String, BackupGym>()

        // Add all local gyms
        local.forEach { gym -> merged[gym.id] = gym }

        // Add server gyms, preferring newer updates
        server.forEach { serverGym ->
            val localGym = merged[serverGym.id]
            if (localGym == null || isNewerThan(serverGym.updatedAt, localGym.updatedAt)) {
                merged[serverGym.id] = serverGym
            }
        }

        return merged.values.toList()
    }

    private fun mergeProblems(
        local: List<BackupProblem>,
        server: List<BackupProblem>,
    ): List<BackupProblem> {
        val merged = mutableMapOf<String, BackupProblem>()

        // Add all local problems
        local.forEach { problem -> merged[problem.id] = problem }

        // Add server problems, preferring newer updates
        server.forEach { serverProblem ->
            val localProblem = merged[serverProblem.id]
            if (localProblem == null || isNewerThan(serverProblem.updatedAt, localProblem.updatedAt)
            ) {
                // Merge image paths to preserve all images
                val allImagePaths = mutableSetOf<String>()
                localProblem?.imagePaths?.let { allImagePaths.addAll(it) }
                serverProblem.imagePaths?.let { allImagePaths.addAll(it) }

                merged[serverProblem.id] =
                    serverProblem.withUpdatedImagePaths(allImagePaths.toList())
            }
        }

        return merged.values.toList()
    }

    private fun mergeSessions(
        local: List<BackupClimbSession>,
        server: List<BackupClimbSession>,
    ): List<BackupClimbSession> {
        val merged = mutableMapOf<String, BackupClimbSession>()

        // Add all local sessions
        local.forEach { session -> merged[session.id] = session }

        // Add server sessions, preferring newer updates
        server.forEach { serverSession ->
            val localSession = merged[serverSession.id]
            if (localSession == null || isNewerThan(serverSession.updatedAt, localSession.updatedAt)
            ) {
                merged[serverSession.id] = serverSession
            }
        }

        return merged.values.toList()
    }

    private fun mergeAttempts(
        local: List<BackupAttempt>,
        server: List<BackupAttempt>,
    ): List<BackupAttempt> {
        val merged = mutableMapOf<String, BackupAttempt>()

        // Add all local attempts
        local.forEach { attempt -> merged[attempt.id] = attempt }

        // Add server attempts, preferring newer updates
        server.forEach { serverAttempt ->
            val localAttempt = merged[serverAttempt.id]
            if (localAttempt == null ||
                isNewerThan(
                    serverAttempt.updatedAt ?: serverAttempt.createdAt,
                    localAttempt.updatedAt ?: localAttempt.createdAt,
                )
            ) {
                merged[serverAttempt.id] = serverAttempt
            }
        }

        return merged.values.toList()
    }

    private fun isNewerThan(dateString1: String, dateString2: String): Boolean {
        return try {
            // Try parsing as instant first
            val date1 = java.time.Instant.parse(dateString1)
            val date2 = java.time.Instant.parse(dateString2)
            date1.isAfter(date2)
        } catch (e: Exception) {
            // Fallback to string comparison
            dateString1 > dateString2
        }
    }

    @Test
    fun `test active sessions excluded from sync`() {
        val allLocalSessions =
            listOf(
                BackupClimbSession(
                    id = "active_session_1",
                    gymId = "gym1",
                    date = "2024-01-01",
                    startTime = "2024-01-01T10:00:00",
                    endTime = null,
                    duration = null,
                    status = SessionStatus.ACTIVE,
                    notes = null,
                    createdAt = "2024-01-01T10:00:00",
                    updatedAt = "2024-01-01T10:00:00",
                ),
                BackupClimbSession(
                    id = "completed_session_1",
                    gymId = "gym1",
                    date = "2023-12-31",
                    startTime = "2023-12-31T15:00:00",
                    endTime = "2023-12-31T17:00:00",
                    duration = 7200000,
                    status = SessionStatus.COMPLETED,
                    notes = "Previous session",
                    createdAt = "2023-12-31T15:00:00",
                    updatedAt = "2023-12-31T17:00:00",
                ),
            )

        // Simulate filtering that would happen in createBackupFromRepository
        val sessionsForSync = allLocalSessions.filter { it.status != SessionStatus.ACTIVE }

        // Only completed sessions should be included in sync
        assertEquals("Should only include completed sessions in sync", 1, sessionsForSync.size)

        // Active session should be excluded
        assertFalse(
            "Should not contain active session in sync",
            sessionsForSync.any {
                it.id == "active_session_1" && it.status == SessionStatus.ACTIVE
            },
        )

        // Completed session should be included
        assertTrue(
            "Should contain completed session in sync",
            sessionsForSync.any {
                it.id == "completed_session_1" && it.status == SessionStatus.COMPLETED
            },
        )
    }
}
