/*
 * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package net.sergeych.lyng.idea.definitions

import com.intellij.testFramework.fixtures.BasePlatformTestCase
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.idea.docs.LyngDocumentationProvider
import net.sergeych.lyng.idea.navigation.LyngPsiReference
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
import net.sergeych.lyng.idea.util.LyngAstManager
import net.sergeych.lyng.miniast.CompletionEngineLight

class LyngDefinitionFilesTest : BasePlatformTestCase() {

    override fun getTestDataPath(): String = ""

    private fun enableCompletion() {
        LyngFormatterSettings.getInstance(project).enableLyngCompletionExperimental = true
    }

    private fun addDefinitionsFile() {
        val defs = """
            /** Utilities exposed via .lyng.d */
            class Declared(val name: String) {
                /** Size property */
                val size: Int = 0
                
                /** Returns greeting. */
                fun greet(who: String): String = "hi " + who
            }
            
            /** Top-level function. */
            fun topFun(x: Int): Int = x + 1
        """.trimIndent()
        myFixture.addFileToProject("api.lyng.d", defs)
    }

    private fun addPlainSymbolsFile() {
        val defs = """
            /** Symbols exposed via include directive */
            class PlainDeclared(val name: String) {
                fun hello(): String = "ok"
            }

            fun plainTopFun(x: Int): Int = x + 2
        """.trimIndent()
        myFixture.addFileToProject("plain_symbols.lyng", defs)
    }

    fun test_CompletionsIncludeDefinitions() {
        addDefinitionsFile()
        enableCompletion()
        run {
            val code = """
                val v = top<caret>
            """.trimIndent()
            myFixture.configureByText("main.lyng", code)
            val text = myFixture.editor.document.text
            val caret = myFixture.caretOffset
            val analysis = LyngAstManager.getAnalysis(myFixture.file)
            val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
            assertTrue("Expected topFun from .lyng.d; got=$engine", engine.contains("topFun"))
        }
        run {
            val code = """
                <caret>
            """.trimIndent()
            myFixture.configureByText("other.lyng", code)
            val text = myFixture.editor.document.text
            val caret = myFixture.caretOffset
            val analysis = LyngAstManager.getAnalysis(myFixture.file)
            val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
            assertTrue("Expected Declared from .lyng.d; got=$engine", engine.contains("Declared"))
        }
    }

    fun test_GotoDefinitionResolvesToDefinitionFile() {
        addDefinitionsFile()
        val code = """
            val x = topFun(1)
            val y = Declared("x")
            y.gre<caret>et("me")
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val offset = myFixture.caretOffset
        val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
        assertNotNull("Expected element at caret for resolve", element)
        val ref = LyngPsiReference(element!!)
        val resolved = ref.resolve()
        assertNotNull("Expected reference to resolve", resolved)
        assertTrue("Expected .lyng.d target; got=${resolved!!.containingFile.name}", resolved.containingFile.name.endsWith(".lyng.d"))
    }

    fun test_QuickDocUsesDefinitionDocs() {
        addDefinitionsFile()
        val code = """
            val y = Declared("x")
            y.gre<caret>et("me")
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val provider = LyngDocumentationProvider()
        val offset = myFixture.caretOffset
        val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
        assertNotNull("Expected element at caret for doc", element)
        val doc = provider.generateDoc(element, element)
        assertNotNull("Expected Quick Doc", doc)
        assertTrue("Doc should include summary; got=$doc", doc!!.contains("Returns greeting"))
    }

    fun test_DiagnosticsIgnoreDefinitionSymbols() {
        addDefinitionsFile()
        val code = """
            val x = topFun(1)
            val y = Declared("x")
            y.greet("me")
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val analysis = LyngAstManager.getAnalysis(myFixture.file)
        val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
        assertTrue("Should not report unresolved name for topFun", messages.none { it.contains("unresolved name: topFun") })
        assertTrue("Should not report unresolved name for Declared", messages.none { it.contains("unresolved name: Declared") })
        assertTrue("Should not report unresolved member for greet", messages.none { it.contains("unresolved member: greet") })
    }

    fun test_DiagnosticsDoNotReportVoidAsUnresolvedName() {
        val code = """
            fun f(): void {
                return void
            }
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val analysis = LyngAstManager.getAnalysis(myFixture.file)
        val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
        assertTrue("Should not report unresolved name for void, got=$messages", messages.none { it.contains("unresolved name: void") })
    }

    fun test_CompletionsIncludePlainLyngViaDirective() {
        addPlainSymbolsFile()
        enableCompletion()
        val code = """
            // include symbols: plain_symbols.lyng
            val v = plainTop<caret>
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val text = myFixture.editor.document.text
        val caret = myFixture.caretOffset
        val analysis = LyngAstManager.getAnalysis(myFixture.file)
        val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
        assertTrue("Expected plainTopFun from included .lyng; got=$engine", engine.contains("plainTopFun"))
    }

    fun test_DiagnosticsIgnorePlainLyngSymbolsViaDirective() {
        addPlainSymbolsFile()
        val code = """
            // include symbols: plain_symbols.lyng
            val x = plainTopFun(1)
            val y = PlainDeclared("x")
            y.hello()
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val analysis = LyngAstManager.getAnalysis(myFixture.file)
        val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
        assertTrue("Should not report unresolved name for plainTopFun", messages.none { it.contains("unresolved name: plainTopFun") })
        assertTrue("Should not report unresolved name for PlainDeclared", messages.none { it.contains("unresolved name: PlainDeclared") })
        assertTrue("Should not report unresolved member for hello", messages.none { it.contains("unresolved member: hello") })
    }

    fun test_DiagnosticsResolveOptionalNetModuleSymbols() {
        val code = """
            import lyng.io.net

            val server = Net.tcpListen(0, "127.0.0.1")
            val port = server.localAddress().port
        """.trimIndent()
        myFixture.configureByText("main.lyng", code)
        val analysis = LyngAstManager.getAnalysis(myFixture.file)
        val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
        assertTrue("Should not report unresolved name for Net, got=$messages", messages.none { it.contains("unresolved name: Net") })
        assertTrue("Should not report unresolved member for tcpListen, got=$messages", messages.none { it.contains("unresolved member: tcpListen") })
        assertTrue("Should not report unresolved member for localAddress, got=$messages", messages.none { it.contains("unresolved member: localAddress") })
        assertTrue("Should not report unresolved member for port, got=$messages", messages.none { it.contains("unresolved member: port") })
    }
}
