/*
 * Copyright (C) 2015-2016 Federico Tomassetti
 * Copyright (C) 2017-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.symbolsolver.resolution.typesolvers;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.cache.Cache;
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
import com.github.javaparser.resolution.model.SymbolReference;
import com.github.javaparser.symbolsolver.cache.InMemoryCache;
import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionClassDeclaration;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver.ExceptionHandlers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;

class CombinedTypeSolverTest extends AbstractTypeSolverTest<CombinedTypeSolver> {

    public CombinedTypeSolverTest() {
        super(CombinedTypeSolver::new);
    }

    static List<Object[]> parameters() {
        // Why these classes? NFE is a subclass, IOOBE is a superclass and ISE is a class without children (by default)
        Predicate<Exception> whitelistTestFilter = ExceptionHandlers.getTypeBasedWhitelist(
                NumberFormatException.class, IndexOutOfBoundsException.class, IllegalStateException.class);
        Predicate<Exception> blacklistTestFilter = ExceptionHandlers.getTypeBasedBlacklist(
                NumberFormatException.class, IndexOutOfBoundsException.class, IllegalStateException.class);

        return Arrays.asList(new Object[][] {
            {new RuntimeException(), ExceptionHandlers.IGNORE_ALL, true}, // 0
            {new RuntimeException(), ExceptionHandlers.IGNORE_NONE, false}, // 1
            {new RuntimeException(), whitelistTestFilter, false}, // 2
            {new IllegalStateException(), whitelistTestFilter, true}, // 3
            {new NumberFormatException(), whitelistTestFilter, true}, // 4
            {new IllegalArgumentException(), whitelistTestFilter, false}, // 5
            {new IndexOutOfBoundsException(), whitelistTestFilter, true}, // 6
            {new ArrayIndexOutOfBoundsException(), whitelistTestFilter, true}, // 7
            {new RuntimeException(), blacklistTestFilter, true}, // 8
            {new NullPointerException(), blacklistTestFilter, true}, // 9
            {new IllegalStateException(), blacklistTestFilter, false}, // 10
            {new NumberFormatException(), blacklistTestFilter, false}, // 11
            {new IllegalArgumentException(), blacklistTestFilter, true}, // 12
            {new IndexOutOfBoundsException(), blacklistTestFilter, false}, // 13
            {new ArrayIndexOutOfBoundsException(), blacklistTestFilter, false}, // 14
        });
    }

    @ParameterizedTest
    @MethodSource("parameters")
    void testExceptionFilter(Exception toBeThrownException, Predicate<Exception> filter, boolean expectForward) {
        TypeSolver erroringTypeSolver = mock(TypeSolver.class);
        when(erroringTypeSolver.getSolvedJavaLangObject())
                .thenReturn(new ReflectionClassDeclaration(Object.class, erroringTypeSolver));
        doThrow(toBeThrownException).when(erroringTypeSolver).tryToSolveType(any(String.class));

        TypeSolver secondaryTypeSolver = mock(TypeSolver.class);
        when(secondaryTypeSolver.getSolvedJavaLangObject())
                .thenReturn(new ReflectionClassDeclaration(Object.class, secondaryTypeSolver));
        when(secondaryTypeSolver.tryToSolveType(any(String.class))).thenReturn(SymbolReference.unsolved());

        try {
            new CombinedTypeSolver(filter, erroringTypeSolver, secondaryTypeSolver)
                    .tryToSolveType("an uninteresting string");
            assertTrue(expectForward, "Forwarded, but we expected an exception");
        } catch (Exception e) {
            assertFalse(expectForward, "Exception, but we expected forwarding"); // if we expected the error to be
            // forwarded there shouldn't be an
            // exception
        }

        verify(secondaryTypeSolver, times(expectForward ? 1 : 0)).tryToSolveType(any(String.class));
    }

    @Test
    public void testConstructorWithList() {
        List<TypeSolver> typeSolverList = new ArrayList<>();
        typeSolverList.add(new ReflectionTypeSolver());
        CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver(typeSolverList);

        SymbolReference<ResolvedReferenceTypeDeclaration> resolved =
                combinedTypeSolver.tryToSolveType(Integer.class.getCanonicalName());
        assertTrue(resolved.isSolved());
    }

    @Test
    public void testConstructorWithArray() {
        CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver(new ReflectionTypeSolver());

        SymbolReference<ResolvedReferenceTypeDeclaration> resolved =
                combinedTypeSolver.tryToSolveType(Integer.class.getCanonicalName());
        assertTrue(resolved.isSolved());
    }

    @Test
    void testConstructorWithNullCache_ShouldThrowNPE() {
        List<TypeSolver> childSolvers = Collections.singletonList(new ReflectionTypeSolver());
        assertThrows(
                NullPointerException.class,
                () -> new CombinedTypeSolver(ExceptionHandlers.IGNORE_NONE, childSolvers, null));
    }

    /**
     * 1. Given a fresh combined type solver, a type is searched in cache and since it doesn't
     *    exist, a new entry should be registered.
     *
     * 2. Given a cache with a cached value, that values should be used.
     */
    @Test
    void testCacheIsUsed_WhenTypeIsRequested() {

        List<TypeSolver> childSolvers = Collections.singletonList(new ReflectionTypeSolver());
        Cache<String, SymbolReference<ResolvedReferenceTypeDeclaration>> cache = spy(InMemoryCache.create());
        CombinedTypeSolver combinedSolver = new CombinedTypeSolver(ExceptionHandlers.IGNORE_NONE, childSolvers, cache);
        SymbolReference<ResolvedReferenceTypeDeclaration> reference;

        // 1.
        reference = combinedSolver.tryToSolveType("java.lang.String");
        assertTrue(reference.isSolved());

        verify(cache).get("java.lang.String");
        verify(cache).put("java.lang.String", reference);
        verifyNoMoreInteractions(cache);

        // Reset the interaction counter for the mock, keeping the
        // cached data unchanged.
        Mockito.reset((Object) cache);

        // 2.
        reference = combinedSolver.tryToSolveType("java.lang.String");
        assertTrue(reference.isSolved());
        verify(cache).get("java.lang.String");
        verifyNoMoreInteractions(cache);
    }

    /**
     * 1. When a new type solver is registered, the cache should be reset.
     */
    @Test
    void testUserAddsNewTypeSolver_CacheShouldBeReset() {
        List<TypeSolver> childSolvers = Collections.singletonList(new ReflectionTypeSolver());
        Cache<String, SymbolReference<ResolvedReferenceTypeDeclaration>> cache = spy(InMemoryCache.create());
        CombinedTypeSolver combinedSolver = new CombinedTypeSolver(ExceptionHandlers.IGNORE_NONE, childSolvers, cache);

        // Try to solve it
        combinedSolver.add(new ReflectionTypeSolver());
        verify(cache).removeAll();
        verifyNoMoreInteractions(cache);
    }
}
