/*
 * Use of this source code is governed by the MIT license that can be
 * found in the LICENSE file.
 */

package org.rust.lang.core.resolve

import com.intellij.psi.PsiDocumentManager
import org.intellij.lang.annotations.Language
import org.rust.CheckTestmarkHit
import org.rust.RsTestBase
import org.rust.checkedResolve
import org.rust.lang.core.psi.ext.RsNamedElement
import org.rust.lang.core.psi.ext.RsReferenceElement
import org.rust.lang.core.resolve.ref.RsResolveCache.Testmarks

class RsResolveCacheTest : RsTestBase() {
    @CheckTestmarkHit(Testmarks.RustStructureDependentCacheCleared::class)
    fun `test cache invalidated on rust structure change`() = checkResolvedToXY("""
        mod a { pub struct S; }
                         //X
        mod b { pub struct S; }
                         //Y
        use a/*caret*/::S;
        type T = S;
               //^
    """, "\bb")

    @CheckTestmarkHit(Testmarks.RemoveChangedElement::class)
    fun `test resolve correctly without global cache invalidation 1`() = checkResolvedToXY("""
        struct S1;
             //X
        struct S2;
             //Y
        fn main() {
            let a: S1/*caret*/;
        }        //^
    """, "\b2")

    @CheckTestmarkHit(Testmarks.RemoveChangedElement::class)
    fun `test resolve correctly without global cache invalidation 2`() = checkResolvedToXY("""
        mod a { pub struct S; }
                         //X
        mod b { pub struct S; }
                         //Y
        fn main() {
            let _: a/*caret*/
                ::S;
                //^
        }
    """, "\bb")

    @CheckTestmarkHit(Testmarks.RemoveChangedElement::class)
    fun `test resolve correctly without global cache invalidation 3`() = checkResolvedToXY("""
        struct S;
        trait Trait1 { type Item; }
                          //X
        trait Trait2 { type Item; }
                          //Y
        impl Trait1 for S { type Item = (); }
        impl Trait2 for S { type Item = (); }
        fn main() {
            let _: <S as Trait1/*caret*/>
                ::Item;
                //^
        }
    """, "\b2")

    fun `test resolve correctly without global cache invalidation 4`() = checkResolvedToXY("""
        mod really_long_name { pub struct S; }
           //Y
        mod bar {
            mod really_long_name { pub struct S; }
               //X
            fn baz() {
                /*caret*/
                really_long_name::S;
            }             //^
        }
    """, "crate::")

    fun `test edit local pat binding`() = checkResolvedAndThenUnresolved("""
        fn main() {
            let a/*caret*/ = 0;
            a;//X
        } //^
    """, "1")

    fun `test edit fn-signature pat binding`() = checkResolvedAndThenUnresolved("""
        fn foo(a/*caret*/: i32) {
             //X
            a;
        } //^
    """, "1")

    fun `test edit label declaration`() = checkResolvedAndThenUnresolved("""
        fn main() {
            'label/*caret*/: loop {
             //X
                break 'label
            }         //^
        }
    """, "1")

    fun `test edit label usage`() = checkResolvedToXY("""
        fn main() {
            'label1: loop {
              //X
                'label2: loop {
                 //Y
                    break 'label1/*caret*/
                }         //^
            }
        }
    """, "\b2")

    fun `test edit local lifetime declaration`() = checkResolvedAndThenUnresolved("""
        fn main() {
            let _: &dyn for<'lifetime/*caret*/>
                            //X
                Trait<'lifetime>;
        }               //^
    """, "1")

    fun `test edit local lifetime usage`() = checkResolvedToXY("""
        fn main() {
            let _: &dyn for<'lifetime1,
                            //X
                            'lifetime2>
                            //Y
                Trait<'lifetime1/*caret*/>;
        }               //^
    """, "\b2")

    fun `test edit fn-signature lifetime declaration`() = checkResolvedAndThenUnresolved("""
        fn foo<'lifetime/*caret*/>() {
                //X
            let _: &dyn Trait<'lifetime>;
        }                     //^
    """, "1")

    fun `test edit fn-signature lifetime usage`() = checkResolvedToXY("""
        fn foo<'lifetime1,
                //X
                'lifetime2>() {
                //Y
            let _: &dyn Trait<'lifetime1/*caret*/>;
        }                     //^
    """, "\b2")

    fun `test edit macro meta variable declaration`() = checkResolvedAndThenUnresolved("""
        macro_rules! foo {
            ($ item_var/*caret*/:item) => {
                //X
                $ item_var
            };    //^
        }
    """, "1")

    fun `test edit macro meta variable usage`() = checkResolvedToXY("""
        macro_rules! foo {
            ($ item_var1:item,
                //X
            $ item_var2:item) => {
                //Y
                $ item_var1/*caret*/
            };    //^
        }
    """, "\b2")

    fun `test struct literal field reference edit field declaration`() = checkResolvedAndThenUnresolved("""
        struct S {
            field/*caret*/: i32
        }   //X
        fn main() {
            S { field: 0 };
        }       //^
    """, "1")

    fun `test struct literal field reference edit usage`() = checkResolvedToXY("""
        struct S {
            field1: i32,
            //X
            field2: i32
        }   //Y
        fn main() {
            S { field1/*caret*/: 0 };
        }       //^
    """, "\b2")

    fun `test edit macro declaration`() = checkResolvedAndThenUnresolved("""
        macro_rules! foo/*caret*/ {
                    //X
            () => { fn foo() {} };
        }
        foo!();
        //^
    """, "1")

    fun `test edit macro call`() = checkResolvedToXY("""
        macro_rules! foo1 {
                    //X
            () => { fn foo() {} };
        }
        macro_rules! foo2 {
                    //Y
            () => { fn foo() {} };
        }
        foo1/*caret*/!();
        //^
    """, "\b2")

    fun `test edit const expr`() = checkResolvedToXY("""
        struct S<const N: usize>;
        fn foo<
            const N1: usize,
            //X
            const N2: usize
            //Y
        >() {
            let _: S<{ N1/*caret*/ }>;
        }             //^
    """, "\b2")

    fun `test edit function-local macro`() = checkResolvedToXY("""
        macro_rules! as_is { ($($ t:tt)*) => { $($ t)* }; }
        struct S1;
             //Y
        fn main() {
            as_is! { struct S1/*caret*/; }
                          //X
            let a: S1;
        }        //^
    """, "\b2")

    override val followMacroExpansions: Boolean
        get() = true

    private fun checkResolvedToXY(@Language("Rust") code: String, textToType: String) {
        InlineFile(code).withCaret()

        val (refElement, _, offset) = findElementWithDataAndOffsetInEditor<RsReferenceElement>("^")

        refElement.checkResolvedTo("X", offset)

        myFixture.type(textToType)
        PsiDocumentManager.getInstance(project).commitAllDocuments() // process PSI modification events
        check(refElement.isValid)

        refElement.checkResolvedTo("Y", offset)
    }

    private fun checkResolvedAndThenUnresolved(@Language("Rust") code: String, textToType: String) {
        InlineFile(code).withCaret()

        val (refElement, _, offset) = findElementWithDataAndOffsetInEditor<RsReferenceElement>("^")

        refElement.checkResolvedTo("X", offset)

        myFixture.type(textToType)
        PsiDocumentManager.getInstance(project).commitAllDocuments() // process PSI modification events
        check(refElement.isValid)

        check(refElement.reference!!.resolve() == null)
    }

    private fun RsReferenceElement.checkResolvedTo(marker: String, offset: Int) {
        val resolved = checkedResolve(offset)
        val target = findElementInEditor<RsNamedElement>(marker)

        check(resolved == target) {
            "$this `${this.text}` should resolve to $target, was $resolved instead"
        }
    }
}
