/*
 * 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.
 *
 */

import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Script
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bridge.data
import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjBuffer
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjString
import net.sergeych.lyng.obj.ObjVoid
import net.sergeych.lyng.toSource
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails

class OOTest {
    @Test
    fun testClassProps() = runTest {
        eval(
            """
            import lyng.time
            
            class Point(val x, val y) {
                static val origin = Point(0,0)
                static var center = null
            }
            Point.center = Point.origin
            assertEquals(Point(0,0), Point.origin)
            assertEquals(Point(0,0), Point.center)
            Point.center = Point(1,2)
            assertEquals(Point(0,0), Point.origin)
            assertEquals(Point(1,2), Point.center)
            
        """.trimIndent()
        )
    }

    @Test
    fun testClassMethods() = runTest {
        eval(
            """
            import lyng.time
            
            var pointData = null
            class Point(val x, val y) {
                static fun getData() = pointData
                static fun setData(value) { 
                    pointData = value + "!"
                }
            }
            assertEquals(Point(0,0), Point(0,0) )
            assertEquals(null, Point.getData() )
            Point.setData("foo")
            assertEquals( "foo!", Point.getData() )
        """.trimIndent()
        )
    }

    @Test
    fun testDynamicGet() = runTest {
        val ms = Script.newScope()
        ms.eval(
            """
            val accessor: String = dynamic {
                get { name ->
                    if( name == "foo" ) "bar" else null
                }
            }
           
            println("-- " + accessor.foo)
            assertEquals("bar", accessor.foo)
            assertEquals(null, accessor.bar)
            
        """.trimIndent()
        )
        ms.eval("""
            assertEquals("bar", accessor.foo)
            assertEquals(null, accessor.bad)
        """.trimIndent())
    }

    @Test
    fun testDelegateSet() = runTest {
        eval(
            """
            var setValueForBar = null
            val accessor: Delegate = dynamic {
                get { name ->
                    when(name) {
                        "foo" -> "bar"
                        "bar" -> setValueForBar 
                        else -> null
                    }
                }
                set { name, value ->
                    if( name == "bar" )
                        setValueForBar = value
                    else throw IllegalAssignmentException("Can't assign "+name)
                }
            }
           
            assertEquals("bar", accessor.foo)
            assertEquals(null, accessor.bar)
            accessor.bar = "buzz"
            assertEquals("buzz", accessor.bar)
            
            assertThrows {
                accessor.bad = "!23"
            }
        """.trimIndent()
        )
    }

    @Test
    fun testDynamicIndexAccess() = runTest {
        eval(
            """
            val store = Map()
            val accessor: Delegate = dynamic {
                get { name ->
                    store[name]
                }
                set { name, value ->
                    store[name] = value
                }
            }
           assertEquals(null, accessor["foo"])
           assertEquals(null, accessor.foo)
           accessor["foo"] = "bar"
           assertEquals("bar", accessor["foo"])
           assertEquals("bar", accessor.foo)
        """.trimIndent()
        )
    }

    @Test
    fun testMultilineConstructor() = runTest {
        eval(
            """
            class Point(
                x,
                y
            ) 
            assertEquals(Point(1,2), Point(1,2) )
            """.trimIndent()
        )
    }

    @Test
    fun testDynamicClass() = runTest {
        eval(
            """
            
            fun getContract(contractName): Delegate {
                dynamic {
                    get { name ->
                        println("Call: %s.%s"(contractName,name))
                    }
                }
            }
            getContract("foo").bar
       """
        )
    }

    @Test
    fun testDynamicClassReturn2() = runTest {
        // todo: should work without extra parenthesis
        // see below
        eval(
            """
            
            fun getContract(contractName): Delegate {
                println("1")
                dynamic {
                    get { name ->
                        println("innrer %s.%s"(contractName,name))
                        { args... ->
                            if( name == "bar" ) (args as List).sum() else null
                        }
                    }
                }
            }
            
            val cc: Delegate = dynamic {
                get { name ->
                    println("Call cc %s"(name))
                    getContract(name)
                }
            }
            
            val x = cc.foo.bar
            println(x)
            x(1,2,3)
            assertEquals(6, x(1,2,3))
            //               v  HERE    v
            assertEquals(15, cc.foo.bar(10,2,3))
       """
        )
    }

    @Test
    fun testClassInitialization() = runTest {
        eval(
            """
            var countInstances = 0
            class Point(val x: Int, val y: Int) {
               println("Class initializer is called 1")
               var magnitude
               
               /*
                    init {} section optionally provide initialization code that is called on each instance creation.
                    it should have the same access to this.* and constructor parameters as any other member
                    function.
               */
               init {
                  countInstances++
                  magnitude = Math.sqrt(x*x + y*y)
               }
            }
            
            val p = Point(1, 2)
            assertEquals(1, countInstances)
            assertEquals(p, Point(1,2) )
            assertEquals(2, countInstances)
            """.trimIndent()
        )
    }

    @Test
    fun testMIInitialization() = runTest {
        eval(
            """
            var order = []
            class A {
                init { order.add("A") }
            }
            class B : A {
                init { order.add("B") }
            }
            class C {
                init { order.add("C") }
            }
            class D : B, C {
                init { order.add("D") }
            }
            D()
            assertEquals(["A", "B", "C", "D"], order)
        """
        )
    }

    @Test
    fun testMIDiamondInitialization() = runTest {
        eval(
            """
            var order = []
            class A {
                init { order.add("A") }
            }
            class B : A {
                init { order.add("B") }
            }
            class C : A {
                init { order.add("C") }
            }
            class D : B, C {
                init { order.add("D") }
            }
            D()
            assertEquals(["A", "B", "C", "D"], order)
        """
        )
    }

    @Test
    fun testInitBlockInDeserialization() = runTest {
        eval(
            """
            import lyng.serialization
            var count = 0
            class A {
                init { count++ }
            }
            val a1 = A()
            val coded = Lynon.encode(a1)
            val a2 = Lynon.decode(coded)
            assertEquals(2, count)
        """
        )
    }

    @Test
    fun testDefaultCompare() = runTest {
        eval(
            """
            class Point(val x: Int, val y: Int) 
            
            assertEquals(Point(1,2), Point(1,2) )
            assert( Point(1,2) != Point(2,1) )
            assert( Point(1,2) == Point(1,2) )
            
        """.trimIndent()
        )
    }

    @Test
    fun testConstructorCallsWithNamedParams() = runTest {
        val scope = Script.newScope()
        val list = scope.eval(
            """
            import lyng.time
            
            class BarRequest(
                    val id,
                    val vaultId, val userAddress, val isDepositRequest, val grossWeight, val fineness, val notes="",
                    val createdAt = Instant.now().truncateToSecond(),
                    val updatedAt = Instant.now().truncateToSecond()
            ) {
                // unrelated for comparison
                static val stateNames = [1, 2, 3]   

                val cell = cached { id }
            }
            assertEquals( 5,5.toInt())
            val b1 = BarRequest(1, "v1", "u1", true, 1000, 999)
            val b2 = BarRequest(1, "v1", "u1", true, 1000, 999, createdAt: b1.createdAt, updatedAt: b1.updatedAt)
            assertEquals(b1, b2)
            assertEquals( 0, b1 <=> b2)
            [b1, b2]  
        """.trimIndent()
        ) as ObjList
        val b1 = list.list[0] as ObjInstance
        val b2 = list.list[1] as ObjInstance
        assertEquals(0, b1.compareTo(scope, b2))
    }

    @Test
    fun testPropAsExtension() = runTest {
        val scope = Script.newScope()
        scope.eval(
            """
            class A(val x) {
                private val privateVal = 100
                val p1 get() = this.x + 1
             }
             assertEquals(2, A(1).p1)
             
             fun A.f() = this.x + 5
             assertEquals(7, __ext__A__f(A(2)))
             
             // The same, we should be able to add member values to a class;
             // notice it should access to the class public instance members, 
             // somewhat like it is declared in the class body
             val A.simple get() = this.x + 3
                          
             assertEquals(5, __ext_get__A__simple(A(2)))
             
             // it should also work with properties:
             val A.p10 get() = this.x * 10
             assertEquals(20, __ext_get__A__p10(A(2)))
        """.trimIndent()
        )

        // important is that such extensions should not be able to access private members
        // and thus remove privateness:
        assertFails {
            scope.eval("val A.exportPrivateVal = privateVal; __ext_get__A__exportPrivateVal(A(1))")
        }
        assertFails {
            scope.eval("val A.exportPrivateValProp get() = privateVal; __ext_get__A__exportPrivateValProp(A(1))")
        }
    }

    @Test
    fun testObjectSingletonSupportsExtensions() = runTest {
        val scope = Script.newScope()
        scope.eval(
            """
            object X {
                fun base() = "base"
            }

            fun X.decorate<T>(value: T): String {
                this.base() + ":" + value.toString()
            }
            val X.tag get() = this.base() + ":tag"

            assertEquals("base", X.base())
            assertEquals("base:42", X.decorate(42))
            assertEquals("base:ok", X.decorate("ok"))
            assertEquals("base:tag", X.tag)

            // Wrapper names should be generated for singleton-object receivers too.
            assertEquals("base:17", __ext__X__decorate(X, 17))
            assertEquals("base:tag", __ext_get__X__tag(X))
        """.trimIndent()
        )
    }

    @Test
    fun testExtensionsAreScopeIsolated() = runTest {
        val scope1 = Script.newScope()
        scope1.eval(
            """
            fun String.totalDigits() =
                // notice using `this`:
                (this.characters as List).filter{ (it as Char).isDigit() }.size()
            assertEquals(2, __ext__String__totalDigits("answer is 42"))
        """
        )
        val scope2 = Script.newScope()
        assertFails {
            // in scope2 we didn't override `totalDigits` extension:
            scope2.eval("""__ext__String__totalDigits("answer is 42")""".trimIndent())
        }
    }

    @Test
    fun testCacheInClass() = runTest {
        eval(
            """
            class T(salt) {
                private var c
                 
                init {
                    println("create cached with "+salt)
                    c = cached { salt + "." }
                }
                
                fun getResult() = c()
            }
            
            val t1 = T("foo")
            val t2 = T("bar")
            assertEquals("bar.", t2.getResult())
            assertEquals("foo.", t1.getResult())
            
        """.trimIndent()
        )
    }

    @Test
    fun testLateInitValsInClasses() = runTest {
        assertFails {
            eval(
                """
                class T {
                    val x
                }
            """
            )
        }

        assertFails {
            eval("val String.late")
        }

        eval(
            """
            // but we can "late-init" them in init block:
            class OK {
                val x
                
                init {
                    x = "foo"
                }
            }
            val ok = OK()
            assertEquals("foo", ok.x)
            
            // they can't be reassigned:
            assertThrows(IllegalAssignmentException) {
                ok.x = "bar"
            }
            
            // To test access before init, we need a trick:
            class AccessBefore {
                val x
                fun readX() = x
                init {
                    assertEquals(x, Unset)
                    // if we call readX() here, x is Unset. 
                    // Just reading it is fine, but using it should throw:
                    assertThrows(UnsetException) { readX() + 1 }
                    x = 42
                }
            }
            AccessBefore()
        """.trimIndent()
        )
    }

    @Test
    fun testPrivateSet() = runTest {
        eval(
            """
            class A {
                var y = 100
                    private set
                fun setValue(newValue) { y = newValue }
            }
            assertEquals(100, A().y)
            assertThrows(IllegalAccessException) { A().y = 200 }
            val a = A()
            a.setValue(200)
            assertEquals(200, a.y)
            
            class B {
                var y = 10
                    protected set
            }
            class C : B {
                fun setBValue(v) { y = v }
            }
            val c = C()
            assertEquals(10, c.y)
            assertThrows(IllegalAccessException) { c.y = 20 }
            c.setBValue(30)
            assertEquals(30, c.y)
            
            class D {
                private var _y = 0
                var y 
                    get() = _y
                    private set(v) { _y = v }
                fun setY(v) { y = v }
            }
            val d = D()
            assertEquals(0, d.y)
            assertThrows(IllegalAccessException) { d.y = 10 }
            d.setY(20)
            assertEquals(20, d.y)
        """
        )
    }

    @Test
    fun testValPrivateSetError() = runTest {
        assertFails {
            eval("class E { val x = 1 private set }")
        }
    }

    @Test
    fun testAbstractClassesAndOverridingProposal() = runTest {
        val scope = Script.newScope()
        /*
            Abstract class is a sort of interface on steroidsL it is a class some members/methods of which
            are required to be implemented by heirs. Still it is a regular class in all other respects.
            Just can't be instantiated
         */
        scope.eval(
            """
            // abstract modifier is required. It can have a constructor, or be without it:    
            abstract class A(someParam=1) {
                // if the method is marked as abstract, it has no body:
                abstract fun foo(): Int
                
                // abstract members have no initializer:
                abstract fun getBar(): Int
            }
            // can't create instance of the abstract class:
            assertThrows { A() }
            """.trimIndent()
        )
        // create abstract method with body or val/var with initializer is an error:
        assertFails { scope.eval("abstract class B { abstract fun foo() = 1 }") }
        assertFails { scope.eval("abstract class C { abstract val bar = 1 }") }

        // inheriting an abstract class without implementing all of it abstract members and methods
        // is not allowed:
        assertFails { scope.eval("class D : A(1) { override fun foo() = 10 }") }

        // but it is allowed to inherit in another abstract class:
        scope.eval("abstract class E : A(1) { override fun foo() = 10 }")

        // implementing all abstracts let us have regular class:
        scope.eval(
            """
            class F : E() {  override fun getBar() = 11 }
            assertEquals(10, F().foo())
            assertEquals(11, F().getBar())
            """.trimIndent()
        )

        // MI-based abstract implementation is deferred.
    }

    @Test
    fun testAbstractAndOverrideEdgeCases() = runTest {
        val scope = Script.newScope()

        // 1. abstract private is an error:
        assertFails { scope.eval("abstract class Err { abstract private fun foo() }") }
        assertFails { scope.eval("abstract class Err { abstract private val x }") }

        // 2. private member in parent is not visible for overriding:
        scope.eval(
            """
            class Base {
                private fun secret() = 1
                fun callSecret() = secret()
            }
            class Derived : Base() {
                // New method name avoids private override ambiguity
                fun secret2() = 2
            }
            val d = Derived()
            assertEquals(2, d.secret2())
            assertEquals(1, d.callSecret())
            """.trimIndent()
        )
        // Using override keyword when there is only a private member in parent is an error:
        assertFails { scope.eval("class D2 : Base() { override fun secret() = 3 }") }

        // 3. interface can have state (constructor, fields, init):
        scope.eval(
            """
            class I(val x) {
                var y = x * 2
                val z
                init {
                    z = y + 1
                }
                fun foo() = x + y + z
            }
            class Impl : I(10)
            val impl = Impl()
            assertEquals(10, impl.x)
            assertEquals(20, impl.y)
            assertEquals(21, impl.z)
            assertEquals(51, impl.foo())
            """.trimIndent()
        )

        // 4. closed members cannot be overridden:
        scope.eval(
            """
            class G {
                closed fun locked() = "locked"
                closed val permanent = 42
            }
            """.trimIndent()
        )
        assertFails { scope.eval("class H : G() { override fun locked() = \"free\" }") }
        assertFails { scope.eval("class H : G() { override val permanent = 0 }") }
        // Even without override keyword, it should fail if it's closed:
        assertFails { scope.eval("class H : G() { fun locked() = \"free\" }") }

        // 5. Visibility widening is allowed, narrowing is forbidden:
        scope.eval(
            """
            class BaseVis {
                protected fun prot() = 1
            }
            class Widened : BaseVis() {
                override fun prot() = 2 // Widened to public (default)
            }
            assertEquals(2, Widened().prot())
            
            class BasePub {
                fun pub() = 1
            }
            """.trimIndent()
        )
        // Narrowing:
        assertFails { scope.eval("class Narrowed : BasePub() { override protected fun pub() = 2 }") }
        assertFails { scope.eval("class Narrowed : BasePub() { override private fun pub() = 2 }") }
    }

    @Test
    fun testInterfaceImplementationByParts() = runTest {
        val scope = Script.newScope()
        scope.eval(
            """
            // Interface with state (id) and abstract requirements
            interface Character {
                abstract val id
                var health
                var mana
                abstract fun getName()
                fun isAlive() = health > 0
                fun status() = getName() + " (#" + id + "): " + health + " HP, " + mana + " MP"
                // name is also abstractly required by the status method, 
                // even if not explicitly marked 'abstract val' here, 
                // it will be looked up in MRO
            }

            class Warrior(id0, health0, mana0) : Character {
                override val id = id0
                override var health = health0
                override var mana = mana0
                override fun getName() = "Hero"
            }

            val w = Warrior(1, 100, 50)
            assertEquals(100, w.health)
            assertEquals(50, w.mana)
            assertEquals(1, w.id)
            assert(w.isAlive())
            assertEquals("Hero (#1): 100 HP, 50 MP", w.status())

            w.health = 0
            assert(!w.isAlive())
            """.trimIndent()
        )
    }
    @Test
    fun testBasicObjectExpression() = runTest {
        eval("""
            val x = object { val y = 1 }
            assertEquals(1, x.y)
            
            class Base(v) {
                val value = v
                fun squares() = value * value
            }
            
            val y = object : Base(2) {
                override val value = 5
            }
            
            assertEquals(25, y.squares())
            
            """.trimIndent())
    }

    @Test
    fun testArgsPriority() = runTest {
        eval("""
            class A(id) {
                var stored = null
                // Arguments should have priority on
                // instance fields
                fun setStored(id) { stored = id }
            }
            val a = A(1)
            assertEquals(1, a.id)
            assertEquals(null, a.stored)
            
            // Check that arguments of the call have the priority:
            a.setStored(2)
            assertEquals(1, a.id)
            assertEquals(2, a.stored)
        """.trimIndent())
    }

    /**
     * Demonstrates that function parameters are shadowed by class methods of the same name
     * when accessed within a block, but not in a single expression.
     */
    @Test
    fun testParameterShadowingConflict() = runTest {
        val scope = Script.newScope()
        val result = scope.eval("""
            class Tester() {
                fun id() { "method" }
                // This correctly returns "success"
                fun checkOk(id) = id
                // This incorrectly returns the 'id' method (a Callable) instead of "success"
                fun checkFail(id) {
                    id
                }
            }
            val t = Tester()
            if (t.checkOk("success") != "success") throw "checkOk failed"
            t.checkFail("success")
        """.trimIndent().toSource("repro"))

        assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block")
    }

    @Test
    fun testOverrideVisibilityRules1() = runTest {
        val scope = Script.newScope()
        scope.eval("""
            interface Base {
                abstract protected fun foo()
                
                fun bar() {
                    // it must see foo() as it is protected and 
                    // is declared here (even as abstract):
                    foo()
                }
            }
            class Derived : Base {
                protected val suffix = "!"
                
                private fun fooPrivateImpl() = "bar"
            
                override protected fun foo() { 
                    // it should access own private and all protected memberes here: 
                    fooPrivateImpl() + suffix  
                }
            }
            class Derived2: Base {
                private var value = 42
                
                private fun fooPrivateImpl() = value
                
                override protected fun foo() {
                    fooPrivateImpl()
                    value++
                }
            }
            val d: Derived = Derived()
            assertEquals("bar!", d.bar())
            val d2: Derived2 = Derived2()
            assertEquals(42, d2.bar())
            assertEquals(43, d2.bar())
        """.trimIndent())
        scope.createChildScope().eval("""            
            val d: Derived = Derived()
            assertEquals("bar!", d.bar())
            val d2: Derived2 = Derived2()
            assertEquals(42, d2.bar())
        """.trimIndent())
    }
    @Test
    fun testOverrideVisibilityRules2() = runTest {
        val scope = Script.newScope()
        scope.eval("""
            interface Base {
                abstract fun foo()
                
                fun bar() {
                    // it must see foo() as it is protected and 
                    // is declared here (even as abstract):
                    foo()
                }
            }
            class Derived : Base {
                protected val suffix = "!"
                
                private fun fooPrivateImpl() = "bar"
            
                override fun foo() { 
                    // it should access own private and all protected memberes here: 
                    fooPrivateImpl() + suffix  
                }
            }
            class Derived2: Base {
                private var value = 42
                
                private fun fooPrivateImpl() = value
                
                override fun foo() {
                    fooPrivateImpl()
                    value++
                }
            }
            val d: Derived = Derived()
            assertEquals("bar!", (d as Derived).bar())
            class Holder {
                val d2: Derived2 = Derived2()
                fun callBar() = (d2 as Derived2).bar()
            }
            val holder: Holder = Holder()
            assertEquals(42, (holder as Holder).callBar())
            assertEquals(43, (holder as Holder).callBar())
        """.trimIndent())
    }

    @Test
    fun testToStringWithTransients() = runTest {
        eval("""
            class C(val amount,@Transient var transient=0) {
                val l by lazy { transient + amount }
                fun lock(): C {
                    if( transient < 10 ) 
                        return C(amount).also { it.transient = transient + 10 }
                    else
                        return this
                }   
            }
            println(C(1))
            val c1: C = C(1).lock() as C
            val c1b: C = c1.lock() as C
            val c2: C = c1b.lock() as C
            println(c1.amount)
            println(c2.amount)
        """.trimIndent())
    }
    @Test
    fun testToStringWithTransient() = runTest {
        eval("""
            class C(val amount,@Transient var transient=0) {
                val l by lazy { transient + amount }
                fun lock(): C {
                    if( transient < 10 ) 
                        return C(amount).also { it.transient = transient + 10 }
                    else
                        return this
                }   
            }
            println(C(1))
            val c1: C = C(1).lock() as C
            val c1b: C = c1.lock() as C
            val c2: C = c1b.lock() as C
            println(c1.amount)
            println(c2.amount)
        """.trimIndent())
    }

    @Test
    fun testToJsonString() = runTest {
        eval("""
            class T(a,b,@Transient c=0)
            assertEquals("{\"a\":\"foo\",\"b\":\"bar\"}",T("foo", "bar").toJsonString())
        """.trimIndent())
    }

    @Test
    fun testAssignToUnqualifiedParams() = runTest {
        eval("""
            class T(x) {
                fun setx(v) { x = v }
                fun incr(v) { x += v }
            }
            val t = T(1)
            t.setx(2)
            assertEquals(2, t.x)
            t.incr(3)
            assertEquals(5, t.x)
        """.trimIndent())
    }
    @Test
    fun testDynamicToDynamic() = runTest {
        val ms = Script.newScope()
        ms.eval("""
  
            class A(prefix) {
                val da = dynamic {
                    get { name -> "a:"+prefix+":"+name }
                }
            }
            
            val B: A = dynamic {
                get { p -> A(p) }
            }
            assertEquals(A("bar").da.foo, "a:bar:foo")
            assertEquals( B.buzz.da.foo, "a:buzz:foo" )
        
            val C = dynamic {
                get { p -> A(p).da }
            }
            
            assertEquals(C.buzz.foo, "a:buzz:foo")
        """.trimIndent())
        ms.eval("""
        """)
    }
    @Test
    fun testDynamicToDynamicFun() = runTest {
        val ms = Script.newScope()
        ms.eval("""
  
            class A(prefix) {
                val da = dynamic {
                    get { name -> { x -> "a:"+prefix+":"+name+"/"+x } }
                }
            }
            
            val B: A = dynamic {
                get { p -> A(p) }
            }
            assertEquals(A("bar").da.foo("buzz"), "a:bar:foo/buzz")
            assertEquals( B.buzz.da.foo("42"), "a:buzz:foo/42" )

            val C = dynamic {
                get { p -> A(p).da }
            }

            assertEquals(C.buzz.foo("one"), "a:buzz:foo/one")
        """.trimIndent())
        ms.eval("""
        """)
    }

    @Test
    fun testExtendingObjectWithExternals() = runTest {
        val s = EvalSession()
        s.eval("""
            extern object Storage {
    
                extern val spaceUsed: Int
                extern val spaceAvailable: Int
            
                /*
                    Return packed binary data or null
                */
                extern fun getPacked(key: String): Buffer?
            
                /*
                    Upsert packed binary data
                */
                extern fun putPacked(key: String,value: Buffer)
            
                /*
                    Delete data.
                    @return true if data were actually deleted, false means
                    there were no data for the key.
                */
                extern fun delete(key: String): Bool
            }
            """.trimIndent()
        )
        val scope = s.getScope() as ModuleScope
        scope.bindObject("Storage") {
            init { _ ->
                data = mutableMapOf<String, ObjBuffer>()
            }
            addVal("spaceUsed") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                ObjInt(storage.values.sumOf { it.size }.toLong())
            }
            addVal("spaceAvailable") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val capacity = 1_024
                ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
            }
            addFun("getPacked") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                storage[key] ?: ObjNull
            }
            addFun("putPacked") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                val value = args.list[1] as ObjBuffer
                storage[key] = value
                ObjVoid
            }
            addFun("delete") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                ObjBool(storage.remove(key) != null)
            }
        }
        s.eval("""
            import lyng.serialization
            
            // Use names that do not collide with Obj built-ins so extension dispatch is exercised.
            override fun Storage.getAt(key: String): Object? {
                Storage.getPacked(key)?.let {
                    Lynon.decode(it.toBitInput())
                }
            }
            
            override fun Storage.putAt(key: String, value: Object) {
                Storage.putPacked(key, Lynon.encode(value).toBuffer())
            }
            
            assertEquals(0, Storage.spaceUsed)
            assertEquals(1024, Storage.spaceAvailable)
            val missing: String? = Storage["missing"]
            assertEquals(null, missing)

            Storage["name"] = "alice"
            Storage["count"] = 42

            val name: String? = Storage["name"]
            val count: Int? = Storage["count"]
            assertEquals("alice", name)
            assertEquals(42, count)
            assert(Storage.spaceUsed > 0)
            assert(Storage.spaceAvailable < 1024)

            val wrappedName: String? = __ext__Storage__getAt(Storage, "name")
            assertEquals("alice", wrappedName)
            __ext__Storage__putAt(Storage, "flag", true)
            val flag: Bool? = Storage["flag"]
            assertEquals(true, flag)

            assert(Storage.delete("name"))
            val deletedName: String? = Storage["name"]
            assertEquals(null, deletedName)
            assert(!Storage.delete("name"))
        """.trimIndent())
    }

    @Test
    fun testExtendingObjectWithExternals2() = runTest {
        val s = EvalSession()
        s.eval("""
            import lyng.serialization
            object Storage {
                extern val spaceUsed: Int
                extern val spaceAvailable: Int
        
                /*
                    Return packed binary data or null
                */
                extern fun getPacked(key: String): Buffer?
        
                /*
                    Upsert packed binary data
                */
                extern fun putPacked(key: String,value: Buffer)
        
                /*
                    Delete data.
                    @return true if data were actually deleted, false means
                    there were no data for the key.
                */
                extern fun delete(key: String): Bool
        
                override fun putAt(key: String,value: Object) {
                    putPacked(key, Lynon.encode(value).toBuffer())
                }
        
                override fun getAt(key: String): Object? =
                        getPacked(key)?.let { Lynon.decode(it.toBitInput()) }
            }

            """.trimIndent()
        )
        val scope = s.getScope() as ModuleScope
        scope.bindObject("Storage") {
            init { _ ->
                data = mutableMapOf<String, ObjBuffer>()
            }
            addVal("spaceUsed") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                ObjInt(storage.values.sumOf { it.size }.toLong())
            }
            addVal("spaceAvailable") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val capacity = 1_024
                ObjInt((capacity - storage.values.sumOf { it.size }).toLong())
            }
            addFun("getPacked") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                storage[key] ?: ObjNull
            }
            addFun("putPacked") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                val value = args.list[1] as ObjBuffer
                storage[key] = value
                ObjVoid
            }
            addFun("delete") {
                val storage = (thisObj as ObjInstance).data as MutableMap<String, ObjBuffer>
                val key = (args.list[0] as ObjString).value
                ObjBool(storage.remove(key) != null)
            }
        }
        s.eval("""
            assertEquals(0, Storage.spaceUsed)
            assertEquals(1024, Storage.spaceAvailable)
            val missing: String? = Storage["missing"]
            assertEquals(null, missing)

            Storage["name"] = "alice"
            Storage["count"] = 42

            val name: String? = Storage["name"]
            val count: Int? = Storage["count"]
            assertEquals("alice", name)
            assertEquals(42, count)
            assert(Storage.spaceUsed > 0)
            assert(Storage.spaceAvailable < 1024)

            val wrappedName: String? = Storage.getAt("name")
            assertEquals("alice", wrappedName)
            Storage.putAt("flag", true)
            val flag: Bool? = Storage["flag"]
            assertEquals(true, flag)

            assert(Storage.delete("name"))
            val deletedName: String? = Storage["name"]
            assertEquals(null, deletedName)
            assert(!Storage.delete("name"))
        """.trimIndent())
    }

    @Test
    fun testForwardSymbolsUsageMustBeAllowed() = runTest {
        eval("""
            class Foo(x) {
                fn fn2() {
                    fn1()
                    println("fn2")
                }
                fn fn1() {
                    println("fn1")
                }
            }

            val foo = Foo(33)
            foo.fn2()
        """.trimIndent())
    }

}
