/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 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.ast.NodeList.nodeList;
import static org.junit.jupiter.api.Assertions.*;

import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.observer.AstObserver;
import com.github.javaparser.ast.observer.ObservableProperty;
import com.github.javaparser.printer.lexicalpreservation.AbstractLexicalPreservingTest;
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
import java.util.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class NodeListTest extends AbstractLexicalPreservingTest {

    @Test
    void replace() {
        final NodeList<Name> list = nodeList(new Name("a"), new Name("b"), new Name("c"));

        final boolean replaced = list.replace(new Name("b"), new Name("z"));

        assertTrue(replaced);
        assertEquals(3, list.size());
        assertEquals("a", list.get(0).asString());
        assertEquals("z", list.get(1).asString());
        assertEquals("c", list.get(2).asString());
    }

    @Test
    void toStringTest() {
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), new Name("cde"));

        assertEquals(3, list.size());
        assertEquals("[abc, bcd, cde]", list.toString());
    }

    @Test
    void addFirst() {
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), new Name("cde"));

        list.addFirst(new Name("xxx"));

        assertEquals(4, list.size());
        assertEquals("[xxx, abc, bcd, cde]", list.toString());
    }

    @Test
    void addLast() {
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), new Name("cde"));

        list.addLast(new Name("xxx"));

        assertEquals(4, list.size());
        assertEquals("[abc, bcd, cde, xxx]", list.toString());
    }

    @Test
    void addBefore() {
        Name n = new Name("bcd");
        final NodeList<Name> list = nodeList(new Name("abc"), n, new Name("cde"));

        list.addBefore(new Name("xxx"), n);

        assertEquals(4, list.size());
        assertEquals("[abc, xxx, bcd, cde]", list.toString());
    }

    @Test
    void addAfter() {
        Name n = new Name("bcd");
        final NodeList<Name> list = nodeList(new Name("abc"), n, new Name("cde"));

        list.addAfter(new Name("xxx"), n);

        assertEquals(4, list.size());
        assertEquals("[abc, bcd, xxx, cde]", list.toString());
    }

    @Test
    void addBeforeFirst() {
        Name abc = new Name("abc");
        final NodeList<Name> list = nodeList(abc, new Name("bcd"), new Name("cde"));

        list.addBefore(new Name("xxx"), abc);

        assertEquals(4, list.size());
        assertEquals("[xxx, abc, bcd, cde]", list.toString());
    }

    @Test
    void addAfterLast() {
        Name cde = new Name("cde");
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), cde);

        list.addAfter(new Name("xxx"), cde);

        assertEquals(4, list.size());
        assertEquals("[abc, bcd, cde, xxx]", list.toString());
    }

    @Test
    public void getFirstWhenEmpty() {
        final NodeList<Name> list = nodeList();

        Optional<Name> first = list.getFirst();

        assertFalse(first.isPresent());
        assertEquals("Optional.empty", first.toString());
    }

    @Test
    public void getFirstWhenNonEmpty() {
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), new Name("cde"));

        Optional<Name> first = list.getFirst();

        assertTrue(first.isPresent());
        assertEquals("Optional[abc]", first.toString());
    }

    @Test
    public void getLastWhenEmpty() {
        final NodeList<Name> list = nodeList();

        Optional<Name> last = list.getLast();

        assertFalse(last.isPresent());
        assertEquals("Optional.empty", last.toString());
    }

    @Test
    public void getLastWhenNonEmpty() {
        final NodeList<Name> list = nodeList(new Name("abc"), new Name("bcd"), new Name("cde"));

        Optional<Name> last = list.getLast();

        assertTrue(last.isPresent());
        assertEquals("Optional[cde]", last.toString());
    }

    @Nested
    class IteratorTest {

        @Nested
        class ObserversTest {
            NodeList<Name> list;
            ListIterator<Name> iterator;

            List<String> propertyChanges;
            List<String> parentChanges;
            List<String> listChanges;
            List<String> listReplacements;
            AstObserver testObserver = new AstObserver() {
                @Override
                public void propertyChange(
                        Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                    propertyChanges.add(String.format(
                            "%s.%s changed from %s to %s",
                            observedNode.getClass().getSimpleName(),
                            property.name().toLowerCase(),
                            oldValue,
                            newValue));
                }

                @Override
                public void parentChange(Node observedNode, Node previousParent, Node newParent) {
                    parentChanges.add(String.format(
                            "%s 's parent changed from %s to %s",
                            observedNode.getClass().getSimpleName(), previousParent, newParent));
                }

                @Override
                public void listChange(
                        NodeList<?> observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
                    listChanges.add(String.format(
                            "%s %s to/from %s at position %d",
                            nodeAddedOrRemoved.getClass().getSimpleName(),
                            type.name(),
                            observedNode.getClass().getSimpleName(),
                            index));
                }

                @Override
                public void listReplacement(NodeList<?> observedNode, int index, Node oldNode, Node newNode) {
                    listReplacements.add(String.format(
                            "%s replaced within %s at position %d",
                            newNode.getClass().getSimpleName(),
                            observedNode.getClass().getSimpleName(),
                            index));
                }
            };

            @BeforeEach
            void pre() {
                list = nodeList();
                list.register(testObserver);
                iterator = list.listIterator();

                propertyChanges = new ArrayList<>();
                parentChanges = new ArrayList<>();
                listChanges = new ArrayList<>();
                listReplacements = new ArrayList<>();
            }

            @Test
            void whenAdd() {
                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(0, listChanges.size());
                assertEquals(0, listReplacements.size());

                iterator.add(new Name("abc"));

                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(1, listChanges.size());
                assertEquals(0, listReplacements.size());

                assertEquals("Name ADDITION to/from NodeList at position 0", listChanges.get(0));
            }

            @Test
            void whenRemove() {
                iterator.add(new Name("abc"));

                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(1, listChanges.size());
                assertEquals(0, listReplacements.size());

                iterator.previous();
                iterator.remove();

                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(2, listChanges.size());
                assertEquals(0, listReplacements.size());

                assertEquals("Name ADDITION to/from NodeList at position 0", listChanges.get(0));
                assertEquals("Name REMOVAL to/from NodeList at position 0", listChanges.get(1));
            }

            @Test
            void whenSet() {
                iterator.add(new Name("abc"));

                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(1, listChanges.size());
                assertEquals(0, listReplacements.size());

                iterator.previous();
                iterator.set(new Name("xyz"));

                assertEquals(0, propertyChanges.size());
                assertEquals(0, parentChanges.size());
                assertEquals(1, listChanges.size());
                assertEquals(1, listReplacements.size());

                assertEquals("Name ADDITION to/from NodeList at position 0", listChanges.get(0));
                assertEquals("Name replaced within NodeList at position 0", listReplacements.get(0));
            }

            @Test
            void usageTest() {
                final String REFERENCE_TO_BE_DELETED = "bad";
                considerCode("" + "@MyAnnotation(myElements = {\"good\", \"bad\", \"ugly\"})\n"
                        + "public final class MyClass {\n"
                        + "}");
                String expected = "" + "@MyAnnotation(myElements = {\"good\", \"ugly\"})\n"
                        + "public final class MyClass {\n"
                        + "}";

                List<NormalAnnotationExpr> annotations = cu.findAll(NormalAnnotationExpr.class);

                annotations.forEach(annotation -> {
                    // testcase, per https://github.com/javaparser/javaparser/issues/2936#issuecomment-731370505
                    MemberValuePair mvp = annotation.getPairs().get(0);
                    Expression value = mvp.getValue();
                    if ((value instanceof ArrayInitializerExpr)) {
                        NodeList<Expression> myElements = ((ArrayInitializerExpr) value).getValues();

                        for (Iterator<Expression> iterator = myElements.iterator(); iterator.hasNext(); ) {
                            Node elt = iterator.next();
                            {
                                String nameAsString = ((StringLiteralExpr) elt).asString();
                                if (REFERENCE_TO_BE_DELETED.equals(nameAsString)) iterator.remove();
                            }
                        }
                    }
                });

                assertEquals(expected, LexicalPreservingPrinter.print(cu));
            }
        }

        @Nested
        class AddRemoveListIteratorTest {
            NodeList<Name> list;
            ListIterator<Name> iterator;

            @BeforeEach
            void pre() {
                list = nodeList();
                iterator = list.listIterator();
            }

            @Test
            void whenAdd() {
                assertFalse(iterator.hasNext());
                assertFalse(iterator.hasPrevious());
                // Note that the element is added before the current cursor, thus is accessible via "previous"
                iterator.add(new Name("abc"));
                assertFalse(iterator.hasNext());
                assertTrue(iterator.hasPrevious());
            }
        }

        @Nested
        class EmptyIteratorTest {
            NodeList<Name> list;
            ListIterator<Name> iterator;

            @BeforeEach
            void pre() {
                list = nodeList();
                iterator = list.listIterator();
            }

            @Test
            void whenNext() {
                assertThrows(NoSuchElementException.class, () -> {
                    iterator.next();
                });
            }

            @Test
            void whenHasNext() {
                assertFalse(iterator.hasNext());
            }

            @Test
            void whenAdd() {
                assertFalse(iterator.hasNext());
                assertFalse(iterator.hasPrevious());
                // Note that the element is added before the current cursor, thus is accessible via "previous"
                iterator.add(new Name("abc"));
                assertFalse(iterator.hasNext());
                assertTrue(iterator.hasPrevious());
            }

            @Test
            void whenSet() {
                assertFalse(iterator.hasNext());
                assertFalse(iterator.hasPrevious());
                assertThrows(IllegalArgumentException.class, () -> {
                    // Note that the cursor is initially at -1, thus not possible to set the value here
                    iterator.set(new Name("abc"));
                });
                // Assert that next/previous are still empty
                assertFalse(iterator.hasNext());
                assertFalse(iterator.hasPrevious());
            }
        }

        @Nested
        class SingleItemIteratorTest {
            NodeList<Name> list;
            Iterator<Name> iterator;

            @BeforeEach
            void pre() {
                list = nodeList(new Name("abc"));
                iterator = list.iterator();
            }

            @Test
            void whenNext() {
                Name next = iterator.next();
                assertNotNull(next);
            }

            @Test
            void whenHasNext() {
                assertTrue(iterator.hasNext());
            }

            @Test
            void whenHasNextRepeated() {
                assertTrue(iterator.hasNext());
                assertTrue(iterator.hasNext());
                assertTrue(iterator.hasNext());
                assertTrue(iterator.hasNext());
            }

            @Test
            void whenHasNextThenNext() {
                assertTrue(iterator.hasNext());
                iterator.next();
                assertFalse(iterator.hasNext());
                assertThrows(NoSuchElementException.class, () -> {
                    iterator.next();
                });
            }

            @Test
            void whenRemove() {
                Name current = iterator.next();
                iterator.remove();
                assertFalse(iterator.hasNext());
                assertThrows(NoSuchElementException.class, () -> {
                    iterator.next();
                });
            }
        }
    }
}
