package de.renew.formalism.java;

import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import de.renew.expression.LocalVariable;
import de.renew.util.Types;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;


/**
 * Unit tests for the ParsedDeclarationNode class.
 */
public class ParsedDeclarationNodeTest {

    /**
     * Tests the constructor of the ParsedDeclarationNode class.
     */
    @Test
    public void testConstructor() {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();

        //when
        Map<String, Class<?>> variables = testee.getVariables();
        Map<String, List<Integer>> variablePositions = testee.getVariablePositions();
        Map<String, Class<?>> wellKnownClasses = testee.getWellKnownClasses();

        //then
        assertNotNull(variables);
        assertNotNull(variablePositions);
        assertNotNull(wellKnownClasses);
    }

    /**
     * Tests the addImport method of the ParsedDeclarationNode class when adding a star import.
     *
     * @throws ParseException if the import cannot be added
     */
    @Test
    public void testAddImport() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        ParsedImportDecl decl = mock(ParsedImportDecl.class);
        decl._star = true;
        decl._name = "com.example.testName";
        Token token = new Token();
        String expectedResult =
            "de.renew.formalism.java.ParsedDeclarationNode(imports=[com.example.testName], "
                + "wellKnownClasses={}, variables={}, anyVariablesDeclared=false)";

        //when
        testee.addImport(decl, token);

        //then
        assertEquals(expectedResult, testee.toString());
    }

    /**
     * Tests the addImport method of the ParsedDeclarationNode class when adding a specific import.
     *
     * @throws ParseException if the import cannot be added
     */
    @Test
    public void testAddImportElseSection() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        ParsedImportDecl decl = mock(ParsedImportDecl.class);
        decl._star = false;
        decl._name = "de.renew.formalism.java.ParsedDeclarationNode";
        Token token = new Token();
        int pos = decl._name.lastIndexOf(".");
        String name = decl._name.substring(pos + 1);

        //when
        testee.addImport(decl, token);

        //then
        assertNotNull(testee.getWellKnownClasses());
        assertEquals(testee.getWellKnownClasses().get(name).getName(), decl._name);
    }

    /**
     * Tests the addImport method of the ParsedDeclarationNode class when adding an invalid import.
     *
     */
    @Test
    public void testAddImportParseException() {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        ParsedImportDecl decl = mock(ParsedImportDecl.class);
        decl._star = false;
        decl._name = "de.renew.formalism.java.random";
        Token token = new Token();

        //when
        //then
        assertThrows(ParseException.class, () -> testee.addImport(decl, token));
        assertEquals(testee.getWellKnownClasses().size(), 0);
    }

    /**
     * Tests the addVariable method of the ParsedDeclarationNode class.
     *
     * @throws ParseException if the variable cannot be added
     */
    @Test
    public void testAddVariable() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        Class<?> clazz = String.class;
        String name = "testVariable";
        Token token = new Token();

        //when
        testee.addVariable(clazz, name, token);

        //then
        assertTrue(testee.getVariables().containsKey("testVariable"));
        assertEquals("java.lang.String", testee.getVariables().get("testVariable").getName());
    }

    /**
     * Tests the addVariable method of the ParsedDeclarationNode class when adding a duplicate variable.
     *
     * @throws ParseException if the variable cannot be added
     */
    @Test
    public void testAddVariableDuplicate() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        Class<?> clazz = String.class;
        String name1 = "testVariable";
        String name2 = "testVariable";
        String errorMessage =
            "Error in line 0, column 0:\n" + "Variable testVariable declared twice.";
        Token token = new Token();

        //when
        testee.addVariable(clazz, name1, token);

        //then
        assertThrows(ParseException.class, () -> {
            testee.addVariable(clazz, name2, token);
        });
        try {
            testee.addVariable(clazz, name2, token);
        } catch (ParseException e) {
            assertEquals(errorMessage, e.getMessage());
        }
    }

    /**
     * Tests the interpreteWellKnownName method of the ParsedDeclarationNode class with a variable.
     *
     * @throws ParseException if the interpretation fails
     */
    @Test
    public void testInterpretWellKnownNameWithVariable() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        String declarationName = "testVariable";
        Class<?> clazz = String.class;
        Token token = new Token();

        //when
        testee.addVariable(clazz, declarationName, token);
        Object result = testee.interpreteWellKnownName("testVariable");

        //then
        assertNotNull(result);
        assertTrue(result instanceof LocalVariable);
    }

    /**
     * Tests the interpreteWellKnownName method of the ParsedDeclarationNode class with a class.
     *
     * @throws ParseException if the interpretation fails
     */
    @Test
    public void testInterpreteWellKnownNameWithClass() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        ParsedImportDecl decl = mock(ParsedImportDecl.class);
        decl._star = false;
        decl._name = "de.renew.formalism.java.ParsedDeclarationNode";
        Token token = new Token();
        int pos = decl._name.lastIndexOf(".");
        String name = decl._name.substring(pos + 1);

        //when
        testee.addImport(decl, token);
        Object result = testee.interpreteWellKnownName(name);

        //then
        assertNotNull(result);
        assertEquals("class " + decl._name, result.toString());
    }

    /**
     * Tests the interpreteName method of the ParsedDeclarationNode class with a qualified class name.
     *
     * @throws LinkageError if the interpretation fails
     */
    @Test
    public void testInterpreteNameQualifiedClass() throws LinkageError {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        String name = "java.util.ArrayList";

        //when
        Object result = testee.interpreteName(name);

        //then
        assertNotNull(result);
        assertEquals("class " + name, result.toString());
    }

    /**
     * Tests the interpreteName method of the ParsedDeclarationNode class with a package specification.
     *
     * @throws LinkageError if the interpretation fails
     */
    @Test
    public void testInterpreteNamePackageSpecification() throws LinkageError {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        String name = "com.example.package";

        //when
        Object result = testee.interpreteName(name);

        //then
        assertNull(result);
    }

    /**
     * Tests the interpreteName method of the ParsedDeclarationNode class with a well-known name.
     *
     * @throws ParseException if the interpretation fails
     */
    @Test
    public void testInterpreteNameWellKnownName() throws ParseException {
        //given
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        ParsedImportDecl decl = mock(ParsedImportDecl.class);
        decl._star = false;
        decl._name = "de.renew.formalism.java.ParsedDeclarationNode";
        Token token = new Token();
        int pos = decl._name.lastIndexOf(".");
        String name = decl._name.substring(pos + 1);

        //when
        testee.addImport(decl, token);
        Object result = testee.interpreteName(name);

        //then
        assertNotNull(result);
        assertEquals(testee.interpreteWellKnownName(name), result);
    }

    /**
     * Tests the findType method of the ParsedDeclarationNode class.
     *
     * @throws ParseException if the type cannot be found
     */
    @Test
    public void testFindType() throws ParseException {
        //given
        String declarationName = "testVariable";
        LocalVariable testVariable = new LocalVariable(declarationName);
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        Class<?> clazz = String.class;
        Token token = new Token();

        //when
        testee.addVariable(clazz, declarationName, token);
        Class<?> result = testee.findType(testVariable);

        //then
        assertNotNull(result);
        assertEquals(String.class, result);
    }

    /**
     * Tests the findType method of the ParsedDeclarationNode class with an untyped variable.
     *
     * @throws ParseException if the type cannot be found
     */
    @Test
    public void testFindTypeUntyped() throws ParseException {
        //given
        String declarationName = "testVariable";
        LocalVariable testVariable = new LocalVariable("example");
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        Class<?> clazz = String.class;
        Token token = new Token();

        //when
        testee.addVariable(clazz, declarationName, token);
        Class<?> result = testee.findType(testVariable);

        //then
        assertNotNull(result);
        assertEquals(Types.UNTYPED, result);
    }

    /**
     * Tests the toString method of the ParsedDeclarationNode class.
     *
     * @throws ParseException if an error occurs during the test
     */
    @Test
    public void testToString() throws ParseException {
        //given
        String declarationName = "testVariable";
        ParsedDeclarationNode testee = new ParsedDeclarationNode();
        Class<?> clazz = String.class;
        Token token = new Token();
        ParsedImportDecl decl1 = mock(ParsedImportDecl.class);
        decl1._star = false;
        decl1._name = "de.renew.formalism.java.ParsedDeclarationNode";
        ParsedImportDecl decl2 = mock(ParsedImportDecl.class);
        decl2._star = true;
        decl2._name = "com.example.testName";
        String expectedResult =
            "de.renew.formalism.java.ParsedDeclarationNode(imports=[com.example.testName],"
                + " wellKnownClasses={ParsedDeclarationNode=de.renew.formalism.java.ParsedDeclarationNode},"
                + " variables={testVariable=class java.lang.String}, anyVariablesDeclared=true)";

        //when
        testee.addVariable(clazz, declarationName, token);
        testee.addImport(decl1, token);
        testee.addImport(decl2, token);

        //then
        assertEquals(expectedResult, testee.toString());
    }
}
