/*
 * 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.symbolsolver.resolution.logic;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.resolution.MethodUsage;
import com.github.javaparser.resolution.Navigator;
import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.declarations.ResolvedInterfaceDeclaration;
import com.github.javaparser.resolution.logic.FunctionalInterfaceLogic;
import com.github.javaparser.symbolsolver.AbstractSymbolResolutionTest;
import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserInterfaceDeclaration;
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class FunctionalInterfaceLogicTest extends AbstractSymbolResolutionTest {

    private TypeSolver typeSolver;

    @BeforeEach
    void setup() throws Exception {
        CombinedTypeSolver combinedtypeSolver = new CombinedTypeSolver();
        combinedtypeSolver.add(new ReflectionTypeSolver());
        typeSolver = combinedtypeSolver;
    }

    /*
     * A simple example of a functional interface
     */
    @Test
    void simpleExampleOfFunctionnalInterface() {
        String code = "interface Runnable {\n" + "    void run();\n" + "}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl =
                cu.findFirst(ClassOrInterfaceDeclaration.class).get();
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * The following interface is not functional because it declares nothing which
     * is not already a member of Object
     */
    @Test
    void notFunctionalBecauseItDeclaresNothingWhichIsNotAlreadyAMemberOfObject() {
        String code = "interface NonFunc {\n" + "    boolean equals(Object obj);\n" + "}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl =
                cu.findFirst(ClassOrInterfaceDeclaration.class).get();
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertFalse(methodUsage.isPresent());
    }

    /*
     * its subinterface can be functional by declaring an abstract method which is
     * not a member of Object
     */
    @Test
    void subinterfaceCanBeFunctionalByDclaringAnAbstractMethodWhichIsNotAMemberOfObject() {
        String code = "interface NonFunc {\n"
                + "    boolean equals(Object obj);\n" + "}\n"
                + "interface Func extends NonFunc {\n"
                + "    int compare(String o1, String o2);\n" + "}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Func");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * the well known interface java.util.Comparator<T> is functional because it has
     * one abstract non-Object method:
     */
    @Test
    void isFunctionalBecauseItHasOneAbstractNonObjectMethod() {
        String code = "interface Comparator<T> {\n"
                + "    boolean equals(Object obj);\n"
                + "    int compare(T o1, T o2);\n" + "}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Comparator");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * The following interface is not functional because while it only declares one
     * abstract method which is not a member of Object, it declares two abstract
     * methods which are not public members of Object:
     */
    @Test
    void isNotFunctionalBecauseItDeclaresTwoAbstractMethodsWhichAreNotPublicMembersOfObject() {
        String code = "interface Foo {\n" + "    int m();\n" + "    Object clone();\n" + "}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Foo");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertFalse(methodUsage.isPresent());
    }

    /*
     * Z is a functional interface because while it inherits two abstract methods
     * which are not members of Object, they have the same signature, so the
     * inherited methods logically represent a single method:
     */
    @Test
    void isFunctionalInterfaceBecauseInheritedAbstractMethodsHaveTheSameSignature() {
        String code = "interface X { int m(Iterable<String> arg); }\n"
                + "interface Y { int m(Iterable<String> arg); }\n"
                + "interface Z extends X, Y {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Z");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * Z is a functional interface in the following interface hierarchy because Y.m
     * is a subsignature of X.m and is return-type-substitutable for X.m:
     */
    @Test
    @Disabled("Return-Type-Substituable must be implemented on reference type")
    void isFunctionalInterfaceBecauseOfSubsignatureAndSubstitutableReturnType() {
        String code = "interface X { Iterable m(Iterable<String> arg); }\n"
                + "interface Y { Iterable<String> m(Iterable arg); }\n"
                + "interface Z extends X, Y {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Z");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * the definition of "functional interface" respects the fact that an interface
     * may only have methods with override-equivalent signatures if one is
     * return-type-substitutable for all the others. Thus, in the following
     * interface hierarchy where Z causes a compile-time error, Z is not a
     * functional interface: (because none of its abstract members are
     * return-type-substitutable for all other abstract members)
     */
    @Test
    void isNotFunctionalInterfaceBecauseNoneOfItsAbstractMembersAreReturnTypeSubstitutableForAllOtherAbstractMembers() {
        String code = "interface X { long m(); }\n" + "interface Y { int m(); }\n" + "interface Z extends X, Y {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Z");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertFalse(methodUsage.isPresent());
    }

    /*
     * In the following example, the declarations of Foo<T,N> and Bar are legal: in
     * each, the methods called m are not subsignatures of each other, but do have
     * different erasures. Still, the fact that the methods in each are not
     * subsignatures means Foo<T,N> and Bar are not functional interfaces. However,
     * Baz is a functional interface because the methods it inherits from
     * Foo<Integer,Integer> have the same signature and so logically represent a
     * single method.
     */
    @Test
    void bazIsAFunctionalInterfaceBecauseMethodsItInheritsFromFooHaveTheSameSignature() {
        String code = "interface Foo<T, N extends Number> {\n"
                + "    void m(T arg);\n"
                + "    void m(N arg);\n"
                + "}\n"
                + "interface Bar extends Foo<String, Integer> {}\n"
                + "interface Baz extends Foo<Integer, Integer> {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Baz");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());

        classOrInterfaceDecl = Navigator.demandInterface(cu, "Bar");
        resolvedDecl = new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertFalse(methodUsage.isPresent());
    }

    /*
     * Functional: signatures are logically "the same"
     */
    @Test
    void withGenericMethodsWithSameSignatures() {
        String code = "interface Action<T> {};\n"
                + "interface X { <T> T execute(Action<T> a); }\n"
                + "interface Y { <S> S execute(Action<S> a); }\n"
                + "interface Exec extends X, Y {}\n";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Exec");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * Error: different signatures, same erasure
     */
    @Test
    void withGenericMethodsWithDifferentSignaturesAndSameErasure() {
        String code = "interface Action<T> {};\n"
                + "interface X { <T>   T execute(Action<T> a); }\n"
                + "interface Y { <S,T> S execute(Action<S> a); }\n"
                + "interface Exec extends X, Y {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Exec");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    /*
     * Functional interfaces can be generic, such as
     * java.util.function.Predicate<T>. Such a functional interface may be
     * parameterized in a way that produces distinct abstract methods - that is,
     * multiple methods that cannot be legally overridden with a single declaration.
     */
    @Test
    @Disabled("Waiting Return-Type-Substituable is fully implemented on reference type.")
    void genericFunctionalInterfacesWithReturnTypeSubstituable() {
        String code = "interface I    { Object m(Class c); }\r\n"
                + "interface J<S> { S m(Class<?> c); }\r\n"
                + "interface K<T> { T m(Class<?> c); }\r\n"
                + "interface Functional<S,T> extends I, J<S>, K<T> {}";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Functional");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }

    @Test
    @Disabled("Waiting Return-Type-Substituable is fully implemented on reference type.")
    void genericFunctionalInterfacesWithGenericParameter() {
        String code =
                "    public interface Foo<T> extends java.util.function.Function<String, T> {\n" + "        @Override\n"
                        + "        T apply(String c);\n"
                        + "    }\n";

        CompilationUnit cu = StaticJavaParser.parse(code);
        ClassOrInterfaceDeclaration classOrInterfaceDecl = Navigator.demandInterface(cu, "Foo");
        ResolvedInterfaceDeclaration resolvedDecl =
                new JavaParserInterfaceDeclaration(classOrInterfaceDecl, typeSolver);
        Optional<MethodUsage> methodUsage = FunctionalInterfaceLogic.getFunctionalMethod(resolvedDecl);
        assertTrue(methodUsage.isPresent());
    }
}
