package com.yorvana.testsupport

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.io.File

class RegressionTierScannerTest {
    @Test
    fun androidTestRegressionSourcesHaveValidTierAnnotations() {
        val regressionRoot = File("src/androidTest/java/com/yorvana/regression")

        val violations = RegressionTierScanner.scanDirectory(regressionRoot)

        assertThat(violations).isEmpty()
    }

    @Test
    fun zeroTierFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import org.junit.Test

                class MissingTierTest {
                    @Test
                    fun runsWithoutTier() = Unit
                }
                """,
            )

        assertThat(violations.single()).contains("has 0 tier annotations")
    }

    @Test
    fun twoTierFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.RegressionFull
                import org.junit.Test

                class TooManyTiersTest {
                    @Regression
                    @RegressionFull
                    @Test
                    fun runsWithTwoTiers() = Unit
                }
                """,
            )

        assertThat(violations.single()).contains("has 2 tier annotations")
    }

    @Test
    fun orphanQuarantineFixtureFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                class OrphanQuarantineTest {
                    @Quarantined(since = "2026-05-22", issue = 120, reason = "orphan")
                    @Test
                    fun quarantinedWithoutTier() = Unit
                }
                """,
            )

        assertThat(violations).contains("com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier has 0 tier annotations")
        assertThat(violations)
            .contains(
                "com.yorvana.regression.OrphanQuarantineTest.quarantinedWithoutTier " +
                    "is quarantined without a tier at the same effective scope",
            )
    }

    @Test
    fun inheritedClassTierFixturePasses() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import org.junit.Test

                @Regression
                open class RegressionBase

                class InheritedTierTest : RegressionBase() {
                    @Test
                    fun inheritsClassTier() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun inheritedClassTiersUsePackageQualifiedLookup() {
        val violations =
            RegressionTierScanner.scanSources(
                mapOf(
                    "src/androidTest/java/com/yorvana/regression/Base.kt" to
                        """
                        package com.yorvana.regression

                        import com.yorvana.testsupport.tiers.Regression
                        import org.junit.Test

                        @Regression
                        open class Base

                        class SamePackageChildTest : Base() {
                            @Test
                            fun inheritsSamePackageTier() = Unit
                        }
                        """.trimIndent(),
                    "src/androidTest/java/com/yorvana/other/Base.kt" to
                        """
                        package com.yorvana.other

                        open class Base
                        """.trimIndent(),
                ),
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun importedSuperclassTiersAreUnsupported() {
        val violations =
            RegressionTierScanner.scanSources(
                mapOf(
                    "src/androidTest/java/com/yorvana/shared/Base.kt" to
                        """
                        package com.yorvana.shared

                        import com.yorvana.testsupport.tiers.Regression

                        @Regression
                        open class Base
                        """.trimIndent(),
                    "src/androidTest/java/com/yorvana/regression/ImportedChildTest.kt" to
                        """
                        package com.yorvana.regression

                        import com.yorvana.shared.Base
                        import org.junit.Test

                        class ImportedChildTest : Base() {
                            @Test
                            fun importedSuperTierIsNotResolved() = Unit
                        }
                        """.trimIndent(),
                ),
            )

        assertThat(violations.single())
            .contains("com.yorvana.regression.ImportedChildTest.importedSuperTierIsNotResolved has 0 tier annotations")
    }

    @Test
    fun annotationArgumentsAndUnknownSiblingsAreAccepted() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                annotation class ScenarioId(val value: String)

                class ForwardCompatibleTest {
                    @Regression
                    @Quarantined(since = "2026-05-22", issue = 120, reason = "fixture")
                    @ScenarioId("R-A01")
                    @Test
                    fun acceptsArgumentsAndUnknownSibling() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun quarantineNamedArgumentsAnyOrderAreAccepted() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                class NamedArgsOrderTest {
                    @Regression
                    @Quarantined(reason = "mixed-order", since = "2026-05-22", issue = 120)
                    @Test
                    fun acceptsMixedOrderArguments() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    @Test
    fun invalidQuarantineFormatFails() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import com.yorvana.testsupport.tiers.Regression
                import com.yorvana.testsupport.tiers.Quarantined
                import org.junit.Test

                class InvalidQuarantineTest {
                    @Regression
                    @Quarantined(since = "2026-5-2", issue = 120, reason = "invalid-date-format")
                    @Test
                    fun invalidDateFormat() = Unit
                }
                """,
            )

        assertThat(violations.single()).contains("Invalid @Quarantined annotation format")
    }

    @Test
    fun unsupportedBacktickTestNamesAreNotScanned() {
        val violations =
            scanFixture(
                """
                package com.yorvana.regression

                import org.junit.Test

                class UnsupportedBacktickNameTest {
                    @Test
                    fun `missing tier slips through because this shape is unsupported`() = Unit
                }
                """,
            )

        assertThat(violations).isEmpty()
    }

    private fun scanFixture(source: String): List<String> = RegressionTierScanner.scanSources(mapOf("Fixture.kt" to source.trimIndent()))
}

private object RegressionTierScanner {
    private val TIER_NAMES = setOf("Regression", "RegressionFull", "BillingSandbox")
    private const val QUARANTINE_NAME = "Quarantined"

    // Source-level guard for simple regression-test shapes only: package declarations, annotations,
    // class headers, same-package inheritance, and @Test function names must be discoverable on
    // single lines. Unsupported Kotlin shapes are documented by fixture tests so future phases can
    // either avoid them under com.yorvana.regression or replace this scanner with a real Kotlin parser.
    fun scanDirectory(root: File): List<String> {
        if (!root.exists()) return emptyList()
        val sources =
            root
                .walkTopDown()
                .filter { it.isFile && it.extension == "kt" }
                .associate { it.path to it.readText() }
        return scanSources(sources)
    }

    fun scanSources(sources: Map<String, String>): List<String> {
        val classes = sources.values.flatMap { source -> parseSource(source) }
        val byFqName = classes.associateBy { it.fqName }
        val violations = mutableListOf<String>()

        sources.forEach { (path, source) ->
            source.lineSequence().forEachIndexed { index, rawLine ->
                val line = rawLine.trim()
                if (line.contains("@Quarantined")) {
                    val hasSince = Regex("""since\s*=\s*"(\d{4}-\d{2}-\d{2})"""").containsMatchIn(line)
                    val hasIssue = Regex("""issue\s*=\s*(\d+)""").containsMatchIn(line)
                    val hasReason = Regex("""reason\s*=\s*"([^"]+)"""").containsMatchIn(line)
                    if (!hasSince || !hasIssue || !hasReason) {
                        violations +=
                            "In $path:${index + 1}: Invalid @Quarantined annotation format: '$line'. " +
                            "Must contain since = \"YYYY-MM-DD\", issue = <number>, and reason = \"...\" in any order."
                    }
                }
            }
        }

        classes.forEach { testClass ->
            testClass.testMethods.forEach { method ->
                val scopes =
                    listOf(
                        AnnotationScope(method.annotations),
                        AnnotationScope(testClass.annotations),
                    ) +
                        inheritedClassScopes(testClass, byFqName)
                val effectiveTierScope = scopes.firstOrNull { scope -> scope.annotations.count { it in TIER_NAMES } > 0 }
                val tierCount = effectiveTierScope?.annotations?.count { it in TIER_NAMES } ?: 0
                val effectiveQuarantineScope = scopes.firstOrNull { QUARANTINE_NAME in it.annotations }

                if (tierCount != 1) {
                    violations += "${testClass.fqName}.${method.name} has $tierCount tier annotations"
                }
                if (effectiveQuarantineScope != null && effectiveQuarantineScope != effectiveTierScope) {
                    violations +=
                        "${testClass.fqName}.${method.name} is quarantined without a tier at the same effective scope"
                }
            }
        }

        return violations
    }

    private fun inheritedClassScopes(
        testClass: ParsedClass,
        byFqName: Map<String, ParsedClass>,
    ): List<AnnotationScope> {
        val scopes = mutableListOf<AnnotationScope>()
        var currentSuperName = testClass.superClassFqName
        val seen = mutableSetOf<String>()
        while (currentSuperName != null && seen.add(currentSuperName)) {
            val superClass = byFqName[currentSuperName] ?: break
            scopes += AnnotationScope(superClass.annotations)
            currentSuperName = superClass.superClassFqName
        }
        return scopes
    }

    private fun parseSource(source: String): List<ParsedClass> {
        val packageName =
            Regex("""^\s*package\s+([A-Za-z_][\w.]*)""", RegexOption.MULTILINE)
                .find(source)
                ?.groupValues
                ?.get(1)
                .orEmpty()
        val classes = mutableListOf<ParsedClass>()
        var currentClass: ParsedClass? = null
        var pendingAnnotations = emptySet<String>()

        source.lineSequence().forEach { rawLine ->
            val line = rawLine.trim()
            val annotations = extractAnnotations(line)
            if (annotations.isNotEmpty()) {
                pendingAnnotations = pendingAnnotations + annotations
            }

            val classMatch = Regex("""\bclass\s+([A-Za-z_]\w*)\s*(?::\s*([A-Za-z_][\w.]*))?""").find(line)
            if (classMatch != null) {
                val simpleName = classMatch.groupValues[1]
                val superClassName =
                    classMatch.groupValues
                        .getOrNull(2)
                        ?.takeIf { it.isNotBlank() }
                currentClass =
                    ParsedClass(
                        packageName = packageName,
                        simpleName = simpleName,
                        superClassFqName = superClassName?.toFqName(packageName),
                        annotations = pendingAnnotations,
                        testMethods = mutableListOf(),
                    )
                classes += currentClass
                pendingAnnotations = emptySet()
                return@forEach
            }

            val methodMatch = Regex("""\bfun\s+([A-Za-z_]\w*)\s*\(""").find(line)
            if (methodMatch != null) {
                if ("Test" in pendingAnnotations) {
                    currentClass?.testMethods?.add(
                        ParsedMethod(
                            name = methodMatch.groupValues[1],
                            annotations = pendingAnnotations,
                        ),
                    )
                }
                pendingAnnotations = emptySet()
            }
        }

        return classes
    }

    private fun String.toFqName(packageName: String): String =
        if ('.' in this) {
            this
        } else {
            listOf(packageName, this).filter { it.isNotBlank() }.joinToString(".")
        }

    private fun extractAnnotations(line: String): Set<String> =
        Regex("""@([A-Za-z_][\w.]*)""")
            .findAll(line)
            .map { it.groupValues[1].substringAfterLast('.') }
            .toSet()

    private data class ParsedClass(
        val packageName: String,
        val simpleName: String,
        val superClassFqName: String?,
        val annotations: Set<String>,
        val testMethods: MutableList<ParsedMethod>,
    ) {
        val fqName: String = listOf(packageName, simpleName).filter { it.isNotBlank() }.joinToString(".")
    }

    private data class ParsedMethod(
        val name: String,
        val annotations: Set<String>,
    )

    private data class AnnotationScope(
        val annotations: Set<String>,
    )
}
