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 GitSandboxEngineTest {
    @Test
    fun statusShowsDeletedTrackedFiles() {
        val repo = RepoState(
            initialized = true,
            files = listOf(GitFile("deleteme.rb", tracked = true, deleted = true)),
            commits = listOf(CommitNode("0000001", "Added a temp file")),
            branches = mapOf("master" to 1),
        )

        val (_, output) = GitSandboxEngine.execute(repo, "git status")

        assertTrue(output.any { it.contains("deleted:") && it.contains("deleteme.rb") })
    }

    @Test
    fun gitRmRemovesDeletedTrackedFileFromIndex() {
        val repo = RepoState(
            initialized = true,
            files = listOf(GitFile("deleteme.rb", tracked = true, deleted = true)),
            commits = listOf(CommitNode("0000001", "Added a temp file")),
            branches = mapOf("master" to 1),
        )

        val (updatedRepo, _) = GitSandboxEngine.execute(repo, "git rm deleteme.rb")

        assertFalse(updatedRepo.files.any { it.name == "deleteme.rb" })
    }

    @Test
    fun rmLevelIsNotSolvedByReadOnlyCommands() {
        val level = rmLevel()
        val repo = level.setup()

        val (updatedRepo, _) = GitSandboxEngine.execute(repo, "ls")

        assertFalse(level.validator(updatedRepo, "ls"))
    }

    @Test
    fun gitMvExpandsWildcardSourcesIntoDestinationDirectory() {
        val repo = RepoState(
            initialized = true,
            files = listOf(
                GitFile("about.html", tracked = true),
                GitFile("contact.html", tracked = true),
                GitFile("index.html", tracked = true),
                GitFile("README", tracked = true),
            ),
            branches = mapOf("master" to 1),
        )

        val (updatedRepo, _) = GitSandboxEngine.execute(repo, "git mv *.html src")

        assertTrue(updatedRepo.files.any { it.name == "src/about.html" })
        assertTrue(updatedRepo.files.any { it.name == "src/contact.html" })
        assertTrue(updatedRepo.files.any { it.name == "src/index.html" })
        assertTrue(updatedRepo.files.any { it.name == "README" })
    }

    @Test
    fun singleQuotedWildcardIsNotExpanded() {
        val repo = RepoState(
            initialized = true,
            files = listOf(GitFile("README"), GitFile("main.kt")),
        )

        val (_, output) = GitSandboxEngine.execute(repo, "echo '*'")

        assertEquals(listOf("*"), output)
    }

    @Test
    fun echoRedirectAcceptsNoSurroundingSpaces() {
        val repo = RepoState(initialized = true)

        val (updatedRepo, output) = GitSandboxEngine.execute(repo, "echo '*.swp'>.gitignore")

        assertTrue(output.isEmpty())
        assertEquals("*.swp", updatedRepo.files.single { it.name == ".gitignore" }.content)
    }

    @Test
    fun commitHashAnswerAcceptsFullHashThatStartsWithDisplayedShortHash() {
        val repo = RepoState(
            initialized = true,
            commits = listOf(CommitNode("abc1234", "Latest commit")),
            branches = mapOf("master" to 1),
        )

        assertTrue(commitHashAnswer("0000001")(repo, "abc1234fedcba9876543210fedcba9876543210"))
    }

    @Test
    fun mdIsAcceptedAsMkdirAlias() {
        val repo = RepoState(initialized = true)

        val (_, output) = GitSandboxEngine.execute(repo, "md src")

        assertTrue(output.isEmpty())
    }

    @Test
    fun dirIsAcceptedAsLsAlias() {
        val repo = RepoState(
            initialized = true,
            files = listOf(GitFile("README"), GitFile("src/main.kt")),
        )

        val (_, output) = GitSandboxEngine.execute(repo, "dir")

        assertTrue("README" in output)
        assertTrue("src/main.kt" in output)
    }

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

        val (_, output) = GitSandboxEngine.execute(repo, "ls")

        assertTrue(".gitignore" in output)
    }

    @Test
    fun lsShowsGitDirectoryEntry() {
        val repo = RepoState(initialized = true)

        val (_, output) = GitSandboxEngine.execute(repo, "ls")

        assertTrue(".git" in output)
    }

    @Test
    fun fileCompletionUsesCurrentDirectory() {
        val repo = RepoState(
            initialized = true,
            currentDir = "src",
            files = listOf(
                GitFile("README"),
                GitFile("src/main.kt"),
                GitFile("src/model/User.kt"),
            ),
        )

        assertEquals(listOf("main.kt", "model/User.kt"), fileCompletionCandidates(repo).sorted())
        assertEquals(listOf("model/"), directoryCompletionCandidates(repo))
    }

    @Test
    fun nativeCompletionUsesCurrentDirectoryIncludingGitMetadata() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-completion").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = level(
                id = "completion-test",
                title = "Completion Test",
                description = "",
                hints = emptyList(),
                commandSuggestions = emptyList(),
                setup = { RepoState(initialized = true) },
                validator = { _, _ -> false },
            )
            val repo = runtime.prepareLevel(level)
            val (gitDirRepo, _) = runtime.execute(level, repo, "cd .git")

            assertTrue("HEAD" in runtime.completionCandidates(level, gitDirRepo, directoriesOnly = false))
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativeCompletionIncludesExecutableShortcutPaths() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-script-completion").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = bisectLevel()
            val repo = runtime.prepareLevel(level)

            val candidates = runtime.completionCandidates(level, repo, directoriesOnly = false)

            assertTrue("./test-balance.sh" in candidates)
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativeGitExecAliasesRefreshWhenBinaryChanges() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-exec-refresh").toFile()
        try {
            val firstGit = File(root, "first-git").also { git.copyTo(it); it.setExecutable(true) }
            val secondGit = File(root, "second-git").also { git.copyTo(it); it.setExecutable(true) }
            val level = level(
                id = "exec-refresh-test",
                title = "Exec Refresh Test",
                description = "",
                hints = emptyList(),
                commandSuggestions = emptyList(),
                setup = { RepoState(initialized = true) },
                validator = { _, _ -> false },
            )

            GitRepositoryRuntime(root, firstGit).prepareLevel(level)
            val alias = File(root, "git-exec/git")
            assertTrue(alias.exists())
            assertTrue(File(root, "git-exec/.binary-fingerprint").readText().contains(firstGit.absolutePath))

            GitRepositoryRuntime(root, secondGit).prepareLevel(level)
            assertTrue(alias.exists())
            assertTrue(File(root, "git-exec/.binary-fingerprint").readText().contains(secondGit.absolutePath))
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativePushLevelHasDivergedStatusAndRebasesCleanly() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-push-level").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = pushLevel()
            val repo = runtime.prepareLevel(level)

            val (_, statusOutput) = runtime.execute(level, repo, "git status")
            assertTrue(statusOutput.any { it.contains("diverged", ignoreCase = true) })

            val (_, diffOutput) = runtime.execute(level, repo, "git diff")
            assertTrue(diffOutput.isNotEmpty())

            val (rebasedRepo, pullOutput) = runtime.execute(level, repo, "git pull --rebase origin master")
            assertFalse(pullOutput.any { it.contains("fatal:", ignoreCase = true) || it.contains("error:", ignoreCase = true) })
            assertTrue(rebasedRepo.files.any { it.name == "file4" && it.tracked })
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativeDiffLevelShowsLine26Change() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-diff-level").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = diffLevel()
            val repo = runtime.prepareLevel(level)

            val appFile = repo.files.single { it.name == "app.rb" }
            assertTrue(appFile.content.contains("server.json"))

            val (_, diffOutput) = runtime.execute(level, repo, "git diff")

            assertTrue(diffOutput.any { it.contains("-  @message = get_response('data.json')") })
            assertTrue(diffOutput.any { it.contains("+  @message = get_response('server.json')") })
            assertTrue(diffOutput.any { it.contains("@@ -23,7 +23,7 @@") })
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativeBlameLevelShowsPasswordAuthor() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-blame-level").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = blameLevel()
            val repo = runtime.prepareLevel(level)

            assertTrue(repo.files.single { it.name == "config.rb" }.content.contains("password"))

            val (_, blameOutput) = runtime.execute(level, repo, "git blame config.rb")

            assertTrue(blameOutput.any { it.contains("Spider Man") && it.contains("password") })
            assertTrue(level.validator(repo, "Spider Man"))
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun cdDotDotShortcutMovesToParentDirectory() {
        val repo = RepoState(initialized = true, currentDir = "src/main")

        val (updatedRepo, output) = GitSandboxEngine.execute(repo, "cd..")

        assertTrue(output.isEmpty())
        assertEquals("src", updatedRepo.currentDir)
    }

    @Test
    fun delIsAcceptedAsRmAlias() {
        val repo = RepoState(
            initialized = true,
            files = listOf(GitFile("deleteme.txt")),
        )

        val (updatedRepo, output) = GitSandboxEngine.execute(repo, "del deleteme.txt")

        assertTrue(output.isEmpty())
        assertFalse(updatedRepo.files.any { it.name == "deleteme.txt" })
    }

    @Test
    fun nativeInteractiveRebaseUsesAppSequenceEditorContent() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-interactive-rebase-editor").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = reorderLevel()
            val repo = runtime.prepareLevel(level)
            val invocation = parseGitEditorInvocation("git rebase -i HEAD~3")
                ?: error("Expected interactive rebase editor invocation")
            val todo = runtime.gitEditorInitialContent(level, repo, invocation)
            val reorderedTodo = todo.lines().toMutableList().also { lines ->
                val secondPick = lines.indexOfFirst { it.contains("Third commit") }
                val thirdPick = lines.indexOfFirst { it.contains("Second commit") }
                val thirdLine = lines[thirdPick]
                lines[thirdPick] = lines[secondPick]
                lines[secondPick] = thirdLine
            }.joinToString("\n")

            val (updatedRepo, output) = runtime.executeGitEditorCommand(level, repo, invocation, reorderedTodo)

            assertFalse(output.any { it.contains("Terminal is dumb", ignoreCase = true) })
            assertTrue(level.validator(updatedRepo, invocation.command))
        } finally {
            root.deleteRecursively()
        }
    }

    @Test
    fun nativeExecutableShortcutRunsScriptThroughShell() {
        val git = testGitBinary()
        assumeTrue(git.exists() && git.canExecute())
        val root = Files.createTempDirectory("githug-script-shortcut").toFile()
        try {
            val runtime = GitRepositoryRuntime(root, git)
            val level = bisectLevel()
            val repo = runtime.prepareLevel(level)

            val (_, output) = runtime.execute(level, repo, "./test-balance.sh")

            assertTrue(output.joinToString("\n"), output.any { it.contains("balance broken") })
        } 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
    }
}
