/* Copyright 2025-2026 Eugene Petrenko (mcp@jonnyzzz.com); Copyright 2025-2026 JetBrains. Use of this source code is governed by the Apache 2.0 license. */
package com.jonnyzzz.mcpSteroid

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.jonnyzzz.mcpSteroid.testHelper.ProjectHomeDirectory
import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors

/**
 * Enforces that `mcp-steroid://` resource URIs in production Kotlin code are constructed
 * from generated prompt article classes rather than hardcoded as string literals.
 *
 * ## Why this exists
 *
 * Prompt resource files in `src/main/prompts/` are compiled at build time into typed Kotlin
 * classes (e.g. `TestSkillPromptArticle`, `DebuggerSkillPromptArticle`) in the package
 * `com.jonnyzzz.mcpSteroid.prompts.generated.*`. Each class has a `.uri` property that
 * returns the canonical `mcp-steroid://...` URI for that resource. When code uses these
 * generated classes, URIs stay in sync with the prompt file structure and any rename or
 * removal is caught at compile time.
 *
 * Hardcoding `"mcp-steroid://prompt/test-skill"` as a raw string bypasses this safety net —
 * if the prompt file is renamed, the string silently becomes a broken reference.
 *
 * ## How to fix a violation
 *
 * When this test fails, it prints the file and line number of each offending literal.
 * Replace the hardcoded URI string with the corresponding generated article class:
 *
 * ```kotlin
 * // BAD — hardcoded URI, breaks silently if prompt file is renamed
 * val uri = "mcp-steroid://prompt/test-skill"
 *
 * // GOOD — compile-time safe, breaks at build time if prompt is renamed/removed
 * val uri = TestSkillPromptArticle().uri
 * ```
 *
 * Generated article classes live in `com.jonnyzzz.mcpSteroid.prompts.generated.*`.
 * To find the right class for a URI, search for the prompt file name in
 * `prompts/build/generated/source/` or look at existing usages in `FetchResourceToolHandler.kt`.
 *
 * ## What the regex matches
 *
 * The pattern `mcp-steroid://\w` matches URIs that reference a specific resource path
 * (e.g. `mcp-steroid://prompt/...`, `mcp-steroid://skill/...`). It does NOT match the bare
 * protocol prefix `mcp-steroid://` followed by non-word characters — mentioning the protocol
 * scheme in code that does URI matching/registration is fine.
 *
 * ## Scope
 *
 * Only production Kotlin sources are scanned (`src/main/kotlin` in ij-plugin, prompts,
 * prompt-generator). Test sources are excluded — tests may legitimately hardcode URIs for
 * assertions. Prompt `.md` files are excluded — they ARE the resource content.
 *
 * ## ALLOWED_FILES
 *
 * If a file legitimately needs the URI protocol prefix for protocol-level handling (not
 * referencing a specific resource), add it to [ALLOWED_FILES] below with a comment explaining
 * why.
 */
class NoHardcodedMcpSteroidUriUsageTest : BasePlatformTestCase() {

    companion object {
        /**
         * Files allowed to contain `mcp-steroid://` literals.
         * Each entry is a path relative to the project root.
         *
         * Add files here ONLY when the URI is protocol-level (e.g. URI scheme matching,
         * prefix stripping) — NOT when referencing a specific resource. For specific
         * resources, use the generated article class: `XxxPromptArticle().uri`.
         */
        private val ALLOWED_FILES = setOf(
            // McpResourceRegistry uses the protocol prefix for URI matching/registration
            "ij-plugin/src/main/kotlin/com/jonnyzzz/mcpSteroid/mcp/McpResourceRegistry.kt",
        )
    }

    fun testNoHardcodedMcpSteroidUriInKotlinSources() {
        val projectHome = ProjectHomeDirectory.requireProjectHomeDirectory()
        // Match specific resource URIs like "mcp-steroid://prompt/skill" but NOT the bare
        // protocol prefix "mcp-steroid://". Mentioning the protocol is fine; hardcoding
        // a specific resource path is what we ban (use generated article classes instead).
        val forbiddenPattern = Regex("""mcp-steroid://\w""")
        // Scope: production Kotlin sources only — ij-plugin + prompts + prompt-generator.
        // Test sources are excluded (tests may legitimately hardcode URIs for assertions).
        // Prompt .md files are excluded (they ARE the resource content).
        val sourceRoots = listOf(
            "ij-plugin/src/main/kotlin",
            "prompts/src/main/kotlin",
            "prompt-generator/src/main/kotlin",
        ).map(projectHome::resolve)

        // Exclude this file itself so it can use the literal without self-evasion tricks.
        val selfPath = projectHome
            .resolve("ij-plugin/src/test/kotlin/com/jonnyzzz/mcpSteroid/NoHardcodedMcpSteroidUriUsageTest.kt")
            .normalize()

        val allowedPaths = ALLOWED_FILES.map { projectHome.resolve(it).normalize() }.toSet()

        val matches = mutableListOf<String>()
        for (root in sourceRoots) {
            if (!Files.isDirectory(root)) continue
            for (file in collectKotlinFiles(root)) {
                val normalized = file.normalize()
                if (normalized == selfPath) continue
                if (normalized in allowedPaths) continue
                val lines = Files.readAllLines(file)
                lines.forEachIndexed { index, line ->
                    if (forbiddenPattern.containsMatchIn(line)) {
                        matches.add("${projectHome.relativize(file)}:${index + 1}")
                    }
                }
            }
        }

        assertTrue(
            "Hardcoded MCP resource URI literals found in Kotlin code:\n${matches.joinToString("\n")}",
            matches.isEmpty()
        )
    }

    private fun collectKotlinFiles(root: Path): List<Path> {
        return Files.walk(root).use { paths ->
            paths
                .filter { it.isKotlinFile() }
                .collect(Collectors.toList())
        }
    }

    private fun Path.isKotlinFile(): Boolean {
        if (!Files.isRegularFile(this)) return false
        val fileName = fileName.toString()
        return fileName.endsWith(".kt") || fileName.endsWith(".kts")
    }
}
