/*
 * Copyright (C) 2013-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.ast;

import static com.github.javaparser.StaticJavaParser.parse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.javaparser.ast.observer.AstObserver;
import com.github.javaparser.ast.observer.AstObserverAdapter;
import com.github.javaparser.ast.observer.ObservableProperty;
import com.github.javaparser.ast.type.PrimitiveType;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

public class ObservationTest {

    @Test
    void registerSubTree() {
        String code = "class A { int f; void foo(int p) { return 'z'; }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {
            @Override
            public void propertyChange(
                    Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                changes.add(String.format(
                        "%s.%s changed from %s to %s",
                        observedNode.getClass().getSimpleName(), property.name().toLowerCase(), oldValue, newValue));
            }
        };
        cu.registerForSubtree(observer);

        assertThat(changes).isEmpty();

        cu.getClassByName("A").get().setName("MyCoolClass");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .getFieldByName("f")
                .get()
                .getVariable(0)
                .setType(new PrimitiveType(PrimitiveType.Primitive.BOOLEAN));
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean");

        cu.getClassByName("MyCoolClass")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getParameterByName("p")
                .get()
                .setName("myParam");
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean",
                        "Parameter.name changed from p to myParam");
    }

    @Test
    void registerWithJustNodeMode() {
        String code = "class A { int f; void foo(int p) { return 'z'; }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {
            @Override
            public void propertyChange(
                    Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                changes.add(String.format(
                        "%s.%s changed from %s to %s",
                        observedNode.getClass().getSimpleName(), property.name().toLowerCase(), oldValue, newValue));
            }
        };
        cu.getClassByName("A").get().register(observer, Node.ObserverRegistrationMode.JUST_THIS_NODE);

        assertThat(changes).isEmpty();

        cu.getClassByName("A").get().setName("MyCoolClass");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .getFieldByName("f")
                .get()
                .getVariable(0)
                .setType(new PrimitiveType(PrimitiveType.Primitive.BOOLEAN));
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getParameterByName("p")
                .get()
                .setName("myParam");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .addField("int", "bar")
                .getVariables()
                .get(0)
                .setInitializer("0");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");
    }

    @Test
    void registerWithNodeAndExistingDescendantsMode() {
        String code = "class A { int f; void foo(int p) { return 'z'; }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {
            @Override
            public void propertyChange(
                    Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                changes.add(String.format(
                        "%s.%s changed from %s to %s",
                        observedNode.getClass().getSimpleName(), property.name().toLowerCase(), oldValue, newValue));
            }
        };
        cu.getClassByName("A")
                .get()
                .register(observer, Node.ObserverRegistrationMode.THIS_NODE_AND_EXISTING_DESCENDANTS);

        assertThat(changes).isEmpty();

        cu.getClassByName("A").get().setName("MyCoolClass");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .getFieldByName("f")
                .get()
                .getVariable(0)
                .setType(new PrimitiveType(PrimitiveType.Primitive.BOOLEAN));
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean");

        cu.getClassByName("MyCoolClass")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getParameterByName("p")
                .get()
                .setName("myParam");
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean",
                        "Parameter.name changed from p to myParam");

        cu.getClassByName("MyCoolClass")
                .get()
                .addField("int", "bar")
                .getVariables()
                .get(0)
                .setInitializer("0");
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean",
                        "Parameter.name changed from p to myParam");
    }

    @Test
    void registerWithSelfPropagatingMode() {
        String code = "class A { int f; void foo(int p) { return 'z'; }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {
            @Override
            public void propertyChange(
                    Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                changes.add(String.format(
                        "%s.%s changed from %s to %s",
                        observedNode.getClass().getSimpleName(), property.name().toLowerCase(), oldValue, newValue));
            }
        };
        cu.getClassByName("A").get().register(observer, Node.ObserverRegistrationMode.SELF_PROPAGATING);

        assertThat(changes).isEmpty();

        cu.getClassByName("A").get().setName("MyCoolClass");
        assertThat(changes).containsExactlyInAnyOrder("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass");

        cu.getClassByName("MyCoolClass")
                .get()
                .getFieldByName("f")
                .get()
                .getVariable(0)
                .setType(new PrimitiveType(PrimitiveType.Primitive.BOOLEAN));
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean");

        cu.getClassByName("MyCoolClass")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getParameterByName("p")
                .get()
                .setName("myParam");
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean",
                        "Parameter.name changed from p to myParam");

        cu.getClassByName("MyCoolClass")
                .get()
                .addField("int", "bar")
                .getVariables()
                .get(0)
                .setInitializer("0");
        assertThat(changes)
                .containsExactlyInAnyOrder(
                        "ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
                        "FieldDeclaration.maximum_common_type changed from int to boolean",
                        "VariableDeclarator.type changed from int to boolean",
                        "Parameter.name changed from p to myParam",
                        "VariableDeclarator.initializer changed from null to 0");
    }

    @Test
    void deleteAParameterTriggerNotifications() {
        String code = "class A { void foo(int p) { }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {

            @Override
            public void listChange(NodeList<?> observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
                changes.add("removing [" + nodeAddedOrRemoved + "] from index " + index);
            }
        };
        cu.register(observer, Node.ObserverRegistrationMode.SELF_PROPAGATING);

        cu.getClassByName("A")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getParameter(0)
                .remove();
        assertThat(changes).containsExactlyInAnyOrder("removing [int p] from index 0");
    }

    @Test
    void deleteClassNameDoesNotTriggerNotifications() {
        String code = "class A { void foo(int p) { }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {

            @Override
            public void listChange(NodeList<?> observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
                changes.add("removing [" + nodeAddedOrRemoved + "] from index " + index);
            }
        };
        cu.register(observer, Node.ObserverRegistrationMode.SELF_PROPAGATING);

        // I cannot remove the name of a type
        assertFalse(cu.getClassByName("A").get().getName().remove());
        assertThat(changes).isEmpty();
    }

    @Test
    void deleteMethodBodyDoesTriggerNotifications() {
        String code = "class A { void foo(int p) { }}";
        CompilationUnit cu = parse(code);
        List<String> changes = new ArrayList<>();
        AstObserver observer = new AstObserverAdapter() {

            @Override
            public void propertyChange(
                    Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                changes.add("setting [" + property + "] to " + newValue);
            }

            @Override
            public void listChange(NodeList<?> observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
                changes.add("removing [" + nodeAddedOrRemoved + "] from index " + index);
            }
        };
        cu.register(observer, Node.ObserverRegistrationMode.SELF_PROPAGATING);

        assertTrue(cu.getClassByName("A")
                .get()
                .getMethodsByName("foo")
                .get(0)
                .getBody()
                .get()
                .remove());
        assertThat(changes).containsExactlyInAnyOrder("setting [BODY] to null");
    }
}
