package solutions.tretter.githugandroid

import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File

class LevelSolutionsTest {
    @Test
    fun everyLevelHasEmbeddedSolutionScenarios() {
        val missing = allGithugLevels().filter { it.testCases.isEmpty() }.map { it.id }

        assertTrue(
            "Expected every level source file to define at least one solution scenario. Missing:\n${missing.joinToString("\n")}",
            missing.isEmpty(),
        )
    }

    @Test
    fun levelIdsAreUnique() {
        val ids = allGithugLevels().map { it.id }

        assertEquals(ids, ids.distinct())
    }

    @Test
    fun implementedLevelOrderMatchesUpstreamExercises() {
        assertEquals(IMPLEMENTED_UPSTREAM_LEVEL_ORDER, allGithugLevels().map { it.id })
    }

    @Test
    fun knownSolutionsCompleteEveryLevel() {
        val evidence = StringBuilder()
        val gitBinary = testGitBinary()
        val runtimeRoot = testSandboxRoot().apply {
            deleteRecursively()
            mkdirs()
        }
        evidence.appendLine("GitHug Android level solution evidence")
        evidence.appendLine("Engine: GitRepositoryRuntime filesystem sandbox")
        evidence.appendLine("Git binary: ${gitBinary.absolutePath}")
        evidence.appendLine("Sandbox root: ${runtimeRoot.absolutePath}")
        evidence.appendLine("Scope: executes embedded solution scenarios through the same runtime and per-level sandbox path used by the app.")
        evidence.appendLine("Levels: ${allGithugLevels().size}")
        evidence.appendLine()

        val failures = allGithugLevels().flatMap { level ->
            val runtime = GitRepositoryRuntime(runtimeRoot, gitBinary)
            evidence.appendLine("================================================================================")
            evidence.appendLine("Level: ${level.id}")
            evidence.appendLine("Title: ${level.title}")
            evidence.appendLine("Initial repo:")
            evidence.append(runtime.prepareLevel(level).describeForEvidence().prependIndent("  "))
            evidence.appendLine()

            level.testCases.mapNotNull { testCase ->
                var repo = runtime.prepareLevel(level)
                var solved = false

                evidence.appendLine("Scenario: ${testCase.name}")
                evidence.appendLine("Solution commands:")
                testCase.commands.forEachIndexed { index, command ->
                    evidence.appendLine("  ${index + 1}. $command")
                }
                evidence.appendLine()

                testCase.commands.forEachIndexed { index, command ->
                    val (nextRepo, output) = runtime.execute(level, repo, command)
                    repo = nextRepo
                    solved = level.validator(repo, command)

                    evidence.appendLine("Command ${index + 1}: $command")
                    evidence.appendLine("Output:")
                    if (output.isEmpty()) {
                        evidence.appendLine("  <no output>")
                    } else {
                        output.forEach { line -> evidence.appendLine("  $line") }
                    }
                    evidence.appendLine("Repo after command:")
                    evidence.append(repo.describeForEvidence().prependIndent("  "))
                    evidence.appendLine("Validator passed after command: $solved")
                    evidence.appendLine()
                }

                evidence.appendLine("Scenario result: ${if (solved) "PASS" else "FAIL"}")
                evidence.appendLine()

                if (solved) null else "${level.id} / ${testCase.name}: ${testCase.commands.joinToString(" && ")}"
            }
        }

        writeEvidenceLog(evidence.toString())

        assertTrue(
            "Expected every known solution to complete its level. Failures:\n${failures.joinToString("\n")}",
            failures.isEmpty(),
        )
    }

    @Test
    fun embeddedSetupChecksPass() {
        val gitBinary = testGitBinary()
        val runtimeRoot = testSandboxRoot().apply {
            deleteRecursively()
            mkdirs()
        }

        val failures = allGithugLevels().flatMap { level ->
            if (level.setupChecks.isEmpty()) return@flatMap emptyList()
            val repo = GitRepositoryRuntime(runtimeRoot, gitBinary).prepareLevel(level)
            level.setupChecks
                .filterNot { check -> check.predicate(repo) }
                .map { check -> "${level.id} / ${check.name}: ${check.failureMessage}" }
        }

        assertTrue(
            "Expected every embedded setup check to pass. Failures:\n${failures.joinToString("\n")}",
            failures.isEmpty(),
        )
    }

    @Test
    fun embeddedNegativeScenariosDoNotSolveLevels() {
        val gitBinary = testGitBinary()
        val runtimeRoot = testSandboxRoot().apply {
            deleteRecursively()
            mkdirs()
        }

        val failures = allGithugLevels().flatMap { level ->
            level.negativeTestCases.mapNotNull { testCase ->
                val runtime = GitRepositoryRuntime(runtimeRoot, gitBinary)
                var repo = runtime.prepareLevel(level)
                var solved = false

                testCase.commands.forEach { command ->
                    val (nextRepo, _) = runtime.execute(level, repo, command)
                    repo = nextRepo
                    solved = level.validator(repo, command)
                }

                if (solved) "${level.id} / ${testCase.name}: ${testCase.commands.joinToString(" && ")}" else null
            }
        }

        assertTrue(
            "Expected every embedded negative scenario to remain unsolved. Failures:\n${failures.joinToString("\n")}",
            failures.isEmpty(),
        )
    }

    private companion object {
        fun writeEvidenceLog(content: String) {
            val repoRoot = File(System.getProperty("user.dir") ?: ".")
            val appDir = if (File(repoRoot, "app/build.gradle.kts").exists()) {
                File(repoRoot, "app")
            } else {
                repoRoot
            }
            val reportDir = File(appDir, "build/reports/level-solutions")
            reportDir.mkdirs()
            File(reportDir, "level-solutions.log").writeText(content)
        }

        fun testSandboxRoot(): File {
            val repoRoot = File(System.getProperty("user.dir") ?: ".")
            val appDir = if (File(repoRoot, "app/build.gradle.kts").exists()) {
                File(repoRoot, "app")
            } else {
                repoRoot
            }
            return File(appDir, "build/test-sandboxes/level-solutions")
        }

        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

            val candidates = listOf(
                File("/usr/bin/git"),
                File("/usr/local/bin/git"),
            )
            candidates.firstOrNull { it.exists() && it.canExecute() }?.let { return it }

            val process = ProcessBuilder("sh", "-c", "command -v git")
                .redirectErrorStream(true)
                .start()
            val output = process.inputStream.bufferedReader().readText().trim()
            val exitCode = process.waitFor()
            assertTrue("A real git executable is required for level solution tests.", exitCode == 0 && output.isNotBlank())
            return File(output)
        }

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

        fun RepoState.describeForEvidence(): String = buildString {
            appendLine("initialized=$initialized")
            appendLine("headBranch=$headBranch")
            appendLine("currentDir=$currentDir")
            appendLine("branches=${branches.toSortedMap()}")
            appendLine("tags=${tags.sorted()}")
            appendLine("remotes=${remotes.toSortedMap()}")
            appendLine("config=${config.toSortedMap()}")
            appendLine("stashes=$stashes")
            appendLine("fetchedBranches=${fetchedBranches.sorted()}")
            appendLine("fetchHeadCount=$fetchHeadCount")
            appendLine("pushedBranches=${pushedBranches.sorted()}")
            appendLine("pushedTags=${pushedTags.sorted()}")
            appendLine("submodules=${submodules.toSortedMap()}")
            appendLine("maintenanceActions=${maintenanceActions.sorted()}")
            appendLine("files=")
            if (files.isEmpty()) {
                appendLine("  <none>")
            } else {
                files.sortedBy { it.name }.forEach { file ->
                    appendLine("  ${file.name} staged=${file.staged} tracked=${file.tracked} content=${file.content.toEvidenceValue()}")
                }
            }
            appendLine("commits=")
            if (commits.isEmpty()) {
                appendLine("  <none>")
            } else {
                commits.forEach { commit ->
                    appendLine("  ${commit.id} parents=${commit.parentCount} ${commit.authorTimestampSeconds ?: "unknown-time"} ${commit.message}")
                }
            }
        }

        fun String.toEvidenceValue(): String {
            if (isEmpty()) return "\"\""
            return lineSequence().joinToString("\\n", prefix = "\"", postfix = "\"")
        }

        val IMPLEMENTED_UPSTREAM_LEVEL_ORDER = listOf(
            "init",
            "config",
            "add",
            "commit",
            "clone",
            "clone_to_folder",
            "ignore",
            "include",
            "status",
            "number_of_files_committed",
            "rm",
            "rm_cached",
            "stash",
            "rename",
            "restructure",
            "log",
            "tag",
            "push_tags",
            "commit_amend",
            "commit_in_future",
            "reset",
            "reset_soft",
            "checkout_file",
            "remote",
            "remote_url",
            "pull",
            "remote_add",
            "push",
            "diff",
            "blame",
            "branch",
            "checkout",
            "checkout_tag",
            "checkout_tag_over_branch",
            "branch_at",
            "delete_branch",
            "push_branch",
            "merge",
            "fetch",
            "rebase",
            "rebase_onto",
            "repack",
            "cherry-pick",
            "grep",
            "rename_commit",
            "squash",
            "merge_squash",
            "reorder",
            "bisect",
            "stage_lines",
            "find_old_branch",
            "revert",
            "restore",
            "conflict",
            "submodule",
        )

    }
}
