package solutions.tretter.githugandroid

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
import java.io.File
import java.nio.file.Files

class InteractiveAddEngineTest {
    @Test
    fun interactiveStageShowsMenuWithoutStagingOrAutoCommands() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

        val (updatedRepo, output) = GitSandboxEngine.execute(repo, "git stage -i")

        assertFalse(updatedRepo.files.single { it.name == "README" }.staged)
        assertTrue(output.any { it.contains("What now>") })
        assertFalse(output.any { it.contains("What now> update") })
        assertFalse(output.any { it.contains("What now> quit") })
        assertFalse(output.any { it.contains("GitHug Android") })
    }

    @Test
    fun interactiveAddShowsMenuWithoutStagingOrAutoCommands() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

        val (updatedRepo, output) = GitSandboxEngine.execute(repo, "git add -i")

        assertFalse(updatedRepo.files.single { it.name == "README" }.staged)
        assertTrue(updatedRepo.interactiveAddSession != null)
        assertTrue(output.any { it.contains("What now>") })
        assertFalse(output.any { it.contains("What now> update") })
        assertFalse(output.any { it.contains("What now> quit") })
    }

    @Test
    fun interactiveAddAcceptsUpdateSelectionFromNextInput() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

        val (menuRepo, _) = GitSandboxEngine.execute(repo, "git stage -i")
        val (updateRepo, updateOutput) = GitSandboxEngine.execute(menuRepo, "2")
        val (selectedRepo, selectionOutput) = GitSandboxEngine.execute(updateRepo, "1")
        val (quitRepo, quitOutput) = GitSandboxEngine.execute(selectedRepo, "7")

        assertTrue(updateRepo.interactiveAddSession?.awaitingUpdateSelection == true)
        assertTrue(updateOutput.any { it.contains("Update>>") })
        assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        assertTrue(selectedRepo.interactiveAddSession?.awaitingUpdateSelection == false)
        assertTrue(selectionOutput.any { it.contains("updated 1 path(s)") })
        assertTrue(quitRepo.interactiveAddSession == null)
        assertTrue(quitOutput.any { it.contains("Bye.") })
    }

    @Test
    fun interactiveAddHandlesEveryDisplayedMenuCommand() {
        val menuCommands = listOf(
            "1" to "What now> 1",
            "2" to "Update>>",
            "3" to "Revert>>",
            "4" to "Add untracked>>",
            "5" to "Patch update>>",
            "6" to "Diff>>",
            "7" to "Bye.",
            "8" to "What now> 8",
        )

        menuCommands.forEach { (command, expectedOutput) ->
            val repo = RepoState(initialized = true, files = listOf(GitFile("README")))
            val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i")
            val (updatedRepo, output) = GitSandboxEngine.execute(menuRepo, command)

            assertFalse("$command should not be rejected", output.any { it.contains("Huh ($command)?") })
            assertTrue("$command should produce $expectedOutput", output.any { it.contains(expectedOutput) })
            if (command in listOf("2", "3", "4", "5", "6")) {
                assertTrue(updatedRepo.interactiveAddSession?.awaitingUpdateSelection == true)
            }
        }
    }

    @Test
    fun interactiveAddHandlesMenuCommandAliases() {
        val aliases = listOf(
            "status" to "What now> status",
            "update" to "Update>>",
            "revert" to "Revert>>",
            "add untracked" to "Add untracked>>",
            "patch" to "Patch update>>",
            "diff" to "Diff>>",
            "quit" to "Bye.",
            "help" to "What now> help",
        )

        aliases.forEach { (command, expectedOutput) ->
            val repo = RepoState(initialized = true, files = listOf(GitFile("README")))
            val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i")
            val (updatedRepo, output) = GitSandboxEngine.execute(menuRepo, command)

            assertFalse("$command should not be rejected", output.any { it.contains("Huh ($command)?") })
            assertTrue("$command should produce $expectedOutput", output.any { it.contains(expectedOutput) })
            if (command in listOf("update", "revert", "add untracked", "patch", "diff")) {
                assertTrue(updatedRepo.interactiveAddSession?.awaitingUpdateSelection == true)
            }
        }
    }

    @Test
    fun interactiveAddPatchSelectionStagesSelectedPath() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

        val (menuRepo, _) = GitSandboxEngine.execute(repo, "git add -i")
        val (patchRepo, patchOutput) = GitSandboxEngine.execute(menuRepo, "patch")
        val (hunkRepo, hunkOutput) = GitSandboxEngine.execute(patchRepo, "1")
        val (selectedRepo, selectionOutput) = GitSandboxEngine.execute(hunkRepo, "y")

        assertTrue(patchRepo.interactiveAddSession?.awaitingUpdateSelection == true)
        assertTrue(patchOutput.any { it.contains("Patch update>>") })
        assertTrue(hunkOutput.any { it.contains("Stage this hunk") })
        assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        assertTrue(selectionOutput.any { it.contains("Stage this hunk") && it.contains("y") })
    }

    @Test
    fun patchAddStartsPatchHunkDialogWithoutStagingImmediately() {
        listOf("git add -p README", "git add --patch README").forEach { command ->
            val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

            val (patchRepo, output) = GitSandboxEngine.execute(repo, command)

            assertFalse("$command should not stage before a selection", patchRepo.files.single { it.name == "README" }.staged)
            assertEquals("patch-hunk", patchRepo.interactiveAddSession?.selectionAction)
            assertTrue(output.any { it.startsWith("diff --git a/README b/README") })
            assertTrue(output.any { it.contains("Stage this hunk") })
        }
    }

    @Test
    fun patchAddSelectionStagesSelectedPath() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README")))

        val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")
        val (selectedRepo, output) = GitSandboxEngine.execute(patchRepo, "y")

        assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        assertTrue(output.any { it.contains("Stage this hunk") && it.contains("y") })
        assertTrue(selectedRepo.interactiveAddSession == null)
    }

    @Test
    fun patchAddHunkEditOpensPatchEditorInvocation() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README", "A\nB\n", tracked = true)))
        val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")

        val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")

        assertEquals(GitEditorCommandKind.PATCH_HUNK, invocation?.kind)
        assertEquals("Edit Patch Hunk", invocation?.title)
        assertTrue(invocation?.initialContent.orEmpty().startsWith("# Manual hunk edit mode -- see bottom for a quick guide."))
        assertFalse(invocation?.initialContent.orEmpty().contains("diff --git a/README b/README"))
        assertFalse(invocation?.initialContent.orEmpty().contains("--- a/README"))
        assertTrue(invocation?.initialContent.orEmpty().contains("# ---"))
        assertTrue(invocation?.initialContent.orEmpty().contains("# To remove '+' lines, delete them."))
        assertTrue(invocation?.initialContent.orEmpty().contains("+A"))
        assertFalse(invocation?.initialContent.orEmpty().contains("Stage this hunk"))
    }

    @Test
    fun editedPatchHunkStagesCurrentPatchTarget() {
        val repo = RepoState(initialized = true, files = listOf(GitFile("README", "A\nB\n", tracked = true)))
        val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")
        val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")
            ?: error("Expected patch editor invocation")

        val (selectedRepo, output) = GitSandboxEngine.applyPatchHunkEdit(patchRepo, invocation.initialContent)

        assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        assertTrue(selectedRepo.interactiveAddSession == null)
        assertTrue(output.any { it.contains("Applied edited hunk.") })
    }

    @Test
    fun patchAddHunkDialogHandlesAdvertisedCommands() {
        val commands = listOf("y", "n", "q", "a", "d", "s", "e", "p", "P", "?")

        commands.forEach { command ->
            val repo = RepoState(initialized = true, files = listOf(GitFile("README", "A\nB\n", tracked = true)))
            val (patchRepo, _) = GitSandboxEngine.execute(repo, "git add -p README")
            val (updatedRepo, output) = GitSandboxEngine.execute(patchRepo, command)

            assertFalse("$command should not be rejected", output.any { it.contains("Unknown command '$command'") })
            assertTrue("$command should echo hunk prompt", output.any { it.contains("Stage this hunk") })
            if (command == "e") {
                assertTrue(output.any { it.contains("Opening patch editor") })
            }
            if (command in listOf("y", "a")) {
                assertTrue(updatedRepo.files.single { it.name == "README" }.staged)
                assertTrue(updatedRepo.interactiveAddSession == null)
            }
        }
    }

    @Test
    fun nativeInteractiveAddSelectionUpdatesGitIndex() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-interactive-add").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = level(
                id = "interactive-add-test",
                title = "Interactive Add Test",
                description = "",
                hints = emptyList(),
                commandSuggestions = emptyList(),
                setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
                validator = { _, _ -> false },
            )
            val repo = runtime.prepareLevel(level)
            val (menuRepo, _) = runtime.execute(level, repo, "git stage -i")
            val (updateRepo, _) = runtime.execute(level, menuRepo, "2")
            val (selectedRepo, _) = runtime.execute(level, updateRepo, "1")
            val (quitRepo, _) = runtime.execute(level, selectedRepo, "7")
            val (_, statusOutput) = runtime.execute(level, quitRepo, "git status")

            assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
            assertTrue(quitRepo.interactiveAddSession == null)
            assertTrue(statusOutput.any { it.contains("new file:") && it.contains("README") })
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativePatchAddSelectionUpdatesGitIndex() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-patch-add").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = level(
                id = "patch-add-test",
                title = "Patch Add Test",
                description = "",
                hints = emptyList(),
                commandSuggestions = emptyList(),
                setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
                validator = { _, _ -> false },
            )
            val repo = runtime.prepareLevel(level)
            val (patchRepo, patchOutput) = runtime.execute(level, repo, "git add -p README")
            val (selectedRepo, _) = runtime.execute(level, patchRepo, "y")

            assertTrue(patchOutput.any { it.contains("Stage this hunk") })
            assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativePatchHunkEditorSaveUpdatesGitIndex() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-patch-edit").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = level(
                id = "patch-edit-test",
                title = "Patch Edit Test",
                description = "",
                hints = emptyList(),
                commandSuggestions = emptyList(),
                setup = { RepoState(initialized = true, files = listOf(GitFile("README")), branches = mapOf("master" to 0)) },
                validator = { _, _ -> false },
            )
            val repo = runtime.prepareLevel(level)
            val (patchRepo, _) = runtime.execute(level, repo, "git add -p README")
            val invocation = GitSandboxEngine.parsePatchHunkEditorInvocation(patchRepo, "e")
                ?: error("Expected patch editor invocation")
            val (selectedRepo, output) = runtime.executeGitEditorCommand(level, patchRepo, invocation, invocation.initialContent)

            assertTrue(output.any { it.contains("Applied edited hunk.") })
            assertTrue(selectedRepo.files.single { it.name == "README" }.staged)
        } finally {
            root.deleteRecursively()
        }
    }

    private fun testGitBinary(): File {
        System.getenv("GITHUG_TEST_GIT_BINARY")
            ?.takeIf { it.isNotBlank() }
            ?.let { File(it) }
            ?.takeIf { it.exists() && it.canExecute() }
            ?.let { return it }

        val repoHostGit = File(repoRoot(), "build/host-git/libgit.so")
        if (repoHostGit.exists() && repoHostGit.canExecute()) return repoHostGit

        return File("/usr/bin/git")
    }

    private fun repoRoot(): File {
        val userDir = File(System.getProperty("user.dir") ?: ".")
        return if (File(userDir, "app/build.gradle.kts").exists()) userDir else userDir.parentFile ?: userDir
    }
}
