abstractTests.ceylon

import ceylon.ast.core {
    Identifier,
    Key,
    Node,
    ScopedKey,
    Visitor
}
import com.redhat.ceylon.compiler.typechecker.tree {
    JNode=Node
}
import ceylon.ast.redhat {
    RedHatTransformer,
    SimpleTokenFactory
}
import ceylon.test {
    assertEquals,
    assertNotEquals,
    test,
    ignore
}
import ceylon.language.meta {
    type
}


Key<JNode> originalNodeKey = ScopedKey<JNode>(`module`, "originalNode");
void storeOriginalNode(JNode originalNode, Node newNode)
        => newNode.put(originalNodeKey, originalNode);
object checkOriginalNodePresence satisfies Visitor {
    shared actual void visitNode(Node node) {
        if (!node.get(originalNodeKey) exists) {
            throw AssertionError("Node ``type(node).declaration.name`` is missing original node");
        }
        node.visitChildren(this);
    }
}

void assertNodesEquals(Node actual, Node expected, String? message = null) {
    assertEquals {
        actual = actual;
        expected = expected;
        message = message;
    };
    if (is Identifier actual) {
        assert (is Identifier expected);
        assertEquals {
            actual = actual.usePrefix;
            expected = expected.usePrefix;
            message = message;
        };
    }
}

void doTest<CeylonAstType,RedHatType>(
    CeylonAstType?(String,Anything(JNode,Node)=) compile,
    RedHatType fromCeylon(RedHatTransformer transformer)(CeylonAstType node), CeylonAstType(RedHatType,Anything(JNode,Node)=) toCeylon,
    <String->CeylonAstType>+ codes)
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    testCompilation(compile, *codes);
    testConversion(fromCeylon, toCeylon, *codes.collect(Entry<String,CeylonAstType>.item));
}

void testConversion<CeylonAstType,RedHatType>(RedHatType fromCeylon(RedHatTransformer transformer)(CeylonAstType node), CeylonAstType(RedHatType,Anything(JNode,Node)=) toCeylon, CeylonAstType+ nodes)
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    for (node in nodes) {
        value converted = toCeylon(fromCeylon(RedHatTransformer(SimpleTokenFactory()))(node), storeOriginalNode);
        assertNodesEquals {
            actual = converted;
            expected = node;
            message = "Double conversion";
        };
        converted.visit(checkOriginalNodePresence);
    }
}

void testCompilation<CeylonAstType>(CeylonAstType?(String,Anything(JNode,Node)=) compile, <String->CeylonAstType>+ codes)
        given CeylonAstType satisfies Node {
    for (code->node in codes) {
        assert (exists compiled = compile(code, storeOriginalNode));
        assertNodesEquals {
            actual = compiled;
            expected = node;
            message = "Compile ‘``code``’";
        };
        compiled.visit(checkOriginalNodePresence);
    }
}

void testEquality<CeylonAstType>(CeylonAstType+ nodes)
        given CeylonAstType satisfies Node {
    for (i->x in nodes.indexed) {
        for (j->y in nodes.indexed) {
            if (i == j) {
                assertEquals(x, y);
            } else {
                assertNotEquals(x, y);
            }
        }
    }
}

// needed for variance – ConcreteTest’s type params
// must be invariant, but we need them 'out' for
// use in AbstractTest
shared interface CodesProvider<out CeylonAstType>
        given CeylonAstType satisfies Node {
    shared formal [<String->CeylonAstType>+] codes;
}

shared interface NodesProvider<out CeylonAstType>
        given CeylonAstType satisfies Node {
    shared formal [CeylonAstType+] nodes;
}

shared interface CompilationTest<out CeylonAstType>
        satisfies CodesProvider<CeylonAstType>
        given CeylonAstType satisfies Node {
    
    shared formal CeylonAstType? compile(String code, Anything(JNode,Node) update = noop);
    
    test
    shared void compilation() => testCompilation(compile, *codes);
}

shared interface ConversionTest<CeylonAstType,RedHatType>
        satisfies NodesProvider<CeylonAstType>
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    
    shared formal RedHatType fromCeylon(RedHatTransformer transformer)(CeylonAstType node);
    shared formal CeylonAstType toCeylon(RedHatType node, Anything(JNode,Node) update = noop);
    
    test
    shared void conversion() => testConversion(fromCeylon, toCeylon, *nodes);
    
    test
    ignore("Not useful during regular development")
    shared void equality() => testEquality(*nodes);
}

shared interface ConcreteTest<CeylonAstType,RedHatType>
        satisfies CompilationTest<CeylonAstType> & ConversionTest<CeylonAstType,RedHatType>
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    
    shared actual [CeylonAstType+] nodes => codes*.item;
}

shared interface AbstractTest<CeylonAstType,RedHatType>
        satisfies CompilationTest<CeylonAstType> & ConversionTest<CeylonAstType,RedHatType>
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    
    shared formal [CodesProvider<CeylonAstType>+] tests;
    
    shared actual [<String->CeylonAstType>+] codes => [for (test in tests) for (code in test.codes) code];
    shared actual [CeylonAstType+] nodes => codes*.item;
}

shared interface AbstractCompilationTest<out CeylonAstType>
        satisfies CompilationTest<CeylonAstType>
        given CeylonAstType satisfies Node {
    
    shared formal [CodesProvider<CeylonAstType>+] tests;
    
    shared actual [<String->CeylonAstType>+] codes => [for (test in tests) for (code in test.codes) code];
}

shared interface AbstractConversionTest<CeylonAstType,RedHatType>
        satisfies ConversionTest<CeylonAstType,RedHatType>
        given CeylonAstType satisfies Node
        given RedHatType satisfies JNode {
    
    shared formal [NodesProvider<CeylonAstType>+] tests;
    
    shared actual [CeylonAstType+] nodes => [for (test in tests) for (node in test.nodes) node];
}