///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2023 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.checks.design;

import static com.google.common.truth.Truth.assertWithMessage;
import static com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck.MSG_KEY;

import java.io.File;
import java.util.Optional;

import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
import com.puppycrawl.tools.checkstyle.DetailAstImpl;
import com.puppycrawl.tools.checkstyle.JavaParser;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;

public class FinalClassCheckTest
    extends AbstractModuleTestSupport {

    @Override
    protected String getPackageLocation() {
        return "com/puppycrawl/tools/checkstyle/checks/design/finalclass";
    }

    @Test
    public void testGetRequiredTokens() {
        final FinalClassCheck checkObj = new FinalClassCheck();
        final int[] expected = {
            TokenTypes.ANNOTATION_DEF,
            TokenTypes.CLASS_DEF,
            TokenTypes.ENUM_DEF,
            TokenTypes.INTERFACE_DEF,
            TokenTypes.RECORD_DEF,
            TokenTypes.CTOR_DEF,
            TokenTypes.PACKAGE_DEF,
            TokenTypes.LITERAL_NEW,
        };
        assertWithMessage("Default required tokens are invalid")
            .that(checkObj.getRequiredTokens())
            .isEqualTo(expected);
    }

    @Test
    public void testFinalClass() throws Exception {
        final String[] expected = {
            "11:1: " + getCheckMessage(MSG_KEY, "InputFinalClass"),
            "19:4: " + getCheckMessage(MSG_KEY, "test4"),
            "117:5: " + getCheckMessage(MSG_KEY, "someinnerClass"),
            "124:5: " + getCheckMessage(MSG_KEY, "SomeClass"),
            "130:5: " + getCheckMessage(MSG_KEY, "SomeClass"),
            "151:1: " + getCheckMessage(MSG_KEY, "TestNewKeyword"),
            "184:5: " + getCheckMessage(MSG_KEY, "NestedClass"),
        };
        verifyWithInlineConfigParser(
                getPath("InputFinalClass.java"), expected);
    }

    @Test
    public void testClassWithPrivateCtorAndNestedExtendingSubclass() throws Exception {
        final String[] expected = {
            "13:9: " + getCheckMessage(MSG_KEY, "ExtendA"),
            "18:9: " + getCheckMessage(MSG_KEY, "ExtendB"),
            "22:5: " + getCheckMessage(MSG_KEY, "C"),
            "24:9: " + getCheckMessage(MSG_KEY, "ExtendC"),
        };
        verifyWithInlineConfigParser(
                getNonCompilablePath(
                        "InputFinalClassClassWithPrivateCtorWithNestedExtendingClass.java"),
                expected);
    }

    @Test
    public void testClassWithPrivateCtorAndNestedExtendingSubclassWithoutPackage()
            throws Exception {
        final String[] expected = {
            "11:9: " + getCheckMessage(MSG_KEY, "ExtendA"),
            "14:5: " + getCheckMessage(MSG_KEY, "C"),
            "16:9: " + getCheckMessage(MSG_KEY, "ExtendC"),
        };
        verifyWithInlineConfigParser(
                getNonCompilablePath(
                "InputFinalClassClassWithPrivateCtorWithNestedExtendingClassWithoutPackage.java"),
                expected);
    }

    @Test
    public void testFinalClassConstructorInRecord() throws Exception {

        final String[] expected = {
            "27:9: " + getCheckMessage(MSG_KEY, "F"),
        };

        verifyWithInlineConfigParser(
                getNonCompilablePath("InputFinalClassConstructorInRecord.java"),
            expected);
    }

    @Test
    public void testImproperToken() {
        final FinalClassCheck finalClassCheck = new FinalClassCheck();
        final DetailAstImpl badAst = new DetailAstImpl();
        final int unsupportedTokenByCheck = TokenTypes.COMPILATION_UNIT;
        badAst.setType(unsupportedTokenByCheck);
        try {
            finalClassCheck.visitToken(badAst);
            assertWithMessage("IllegalStateException is expected").fail();
        }
        catch (IllegalStateException ex) {
            assertWithMessage("Invalid exception message")
                .that(ex.getMessage())
                .isEqualTo(badAst.toString());
        }
    }

    @Test
    public void testGetAcceptableTokens() {
        final FinalClassCheck obj = new FinalClassCheck();
        final int[] expected = {
            TokenTypes.ANNOTATION_DEF,
            TokenTypes.CLASS_DEF,
            TokenTypes.ENUM_DEF,
            TokenTypes.INTERFACE_DEF,
            TokenTypes.RECORD_DEF,
            TokenTypes.CTOR_DEF,
            TokenTypes.PACKAGE_DEF,
            TokenTypes.LITERAL_NEW,
        };
        assertWithMessage("Default acceptable tokens are invalid")
            .that(obj.getAcceptableTokens())
            .isEqualTo(expected);
    }

    @Test
    public void testFinalClassInnerAndNestedClasses() throws Exception {
        final String[] expected = {
            "16:5: " + getCheckMessage(MSG_KEY, "SubClass"),
            "19:5: " + getCheckMessage(MSG_KEY, "SameName"),
            "45:9: " + getCheckMessage(MSG_KEY, "SameName"),
            "69:13: " + getCheckMessage(MSG_KEY, "B"),
            "84:9: " + getCheckMessage(MSG_KEY, "c"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassInnerAndNestedClass.java"), expected);
    }

    @Test
    public void testFinalClassStaticNestedClasses() throws Exception {

        final String[] expected = {
            "14:17: " + getCheckMessage(MSG_KEY, "C"),
            "32:9: " + getCheckMessage(MSG_KEY, "B"),
            "43:9: " + getCheckMessage(MSG_KEY, "C"),
            "60:13: " + getCheckMessage(MSG_KEY, "Q"),
            "76:9: " + getCheckMessage(MSG_KEY, "F"),
            "83:9: " + getCheckMessage(MSG_KEY, "c"),
        };

        verifyWithInlineConfigParser(
                getNonCompilablePath("InputFinalClassNestedStaticClassInsideInnerClass.java"),
                expected);
    }

    @Test
    public void testFinalClassEnum() throws Exception {
        final String[] expected = {
            "35:5: " + getCheckMessage(MSG_KEY, "DerivedClass"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassEnum.java"), expected);
    }

    @Test
    public void testFinalClassAnnotation() throws Exception {
        final String[] expected = {
            "15:5: " + getCheckMessage(MSG_KEY, "DerivedClass"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassAnnotation.java"), expected);
    }

    @Test
    public void testFinalClassInterface() throws Exception {
        final String[] expected = {
            "15:5: " + getCheckMessage(MSG_KEY, "DerivedClass"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassInterface.java"), expected);
    }

    @Test
    public void testFinalClassAnonymousInnerClass() throws Exception {
        final String[] expected = {
            "11:9: " + getCheckMessage(MSG_KEY, "b"),
            "27:9: " + getCheckMessage(MSG_KEY, "m"),
            "40:9: " + getCheckMessage(MSG_KEY, "q"),
            "52:13: " + getCheckMessage(MSG_KEY, "b"),
            "67:9: " + getCheckMessage(MSG_KEY, "g"),
            "71:9: " + getCheckMessage(MSG_KEY, "y"),
            "84:9: " + getCheckMessage(MSG_KEY, "n"),
            "91:9: " + getCheckMessage(MSG_KEY, "n"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassAnonymousInnerClass.java"), expected);
    }

    @Test
    public void testFinalClassNestedInInterface() throws Exception {
        final String[] expected = {
            "24:5: " + getCheckMessage(MSG_KEY, "b"),
            "28:13: " + getCheckMessage(MSG_KEY, "m"),
            "50:5: " + getCheckMessage(MSG_KEY, "c"),
        };
        verifyWithInlineConfigParser(
            getPath("InputFinalClassNestedInInterfaceWithAnonInnerClass.java"), expected);
    }

    @Test
    public void testFinalClassNestedInEnum() throws Exception {
        final String[] expected = {
            "13:9: " + getCheckMessage(MSG_KEY, "j"),
            "27:9: " + getCheckMessage(MSG_KEY, "n"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassNestedInEnumWithAnonInnerClass.java"),
                                     expected);
    }

    @Test
    public void testFinalClassNestedInRecord() throws Exception {
        final String[] expected = {
            "13:9: " + getCheckMessage(MSG_KEY, "c"),
            "31:13: " + getCheckMessage(MSG_KEY, "j"),
            "49:5: " + getCheckMessage(MSG_KEY, "Nothing"),
        };
        verifyWithInlineConfigParser(getNonCompilablePath("InputFinalClassNestedInRecord.java"),
                                     expected);
    }

    /**
     * We cannot reproduce situation when visitToken is called and leaveToken is not.
     * So, we have to use reflection to be sure that even in such situation
     * state of the field will be cleared.
     *
     * @throws Exception when code tested throws exception
     */
    @Test
    public void testClearState() throws Exception {
        final FinalClassCheck check = new FinalClassCheck();
        final DetailAST root = JavaParser.parseFile(new File(getPath("InputFinalClass.java")),
                JavaParser.Options.WITHOUT_COMMENTS);
        final Optional<DetailAST> packageDef = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.PACKAGE_DEF);

        assertWithMessage("Ast should contain PACKAGE_DEF")
                .that(packageDef.isPresent())
                .isTrue();
        assertWithMessage("State is not cleared on beginTree")
                .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, packageDef.get(),
                        "packageName", packageName -> ((String) packageName).isEmpty()))
                .isTrue();
    }

    @Test
    public void testPrivateClassWithDefaultCtor() throws Exception {
        final String[] expected = {
            "14:5: " + getCheckMessage(MSG_KEY, "Some2"),
            "19:1: " + getCheckMessage(MSG_KEY, "Some"),
            "24:5: " + getCheckMessage(MSG_KEY, "Some3"),
            "26:5: " + getCheckMessage(MSG_KEY, "Some4"),
            "31:5: " + getCheckMessage(MSG_KEY, "PaperSetter"),
            "36:5: " + getCheckMessage(MSG_KEY, "Paper"),
            "44:5: " + getCheckMessage(MSG_KEY, "Node"),
            "51:5: " + getCheckMessage(MSG_KEY, "Some1"),
            "55:1: " + getCheckMessage(MSG_KEY, "Some2"),
            "105:5: " + getCheckMessage(MSG_KEY, "NewCheck"),
            "108:5: " + getCheckMessage(MSG_KEY, "NewCheck2"),
            "112:5: " + getCheckMessage(MSG_KEY, "OldCheck"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassPrivateCtor.java"),
                                     expected);
    }

    @Test
    public void testPrivateClassWithDefaultCtor2() throws Exception {
        final String[] expected = {
            "22:5: " + getCheckMessage(MSG_KEY, "PrivateClass"),
            "34:5: " + getCheckMessage(MSG_KEY, "Check"),
            "44:5: " + getCheckMessage(MSG_KEY, "K"),
            "54:5: " + getCheckMessage(MSG_KEY, "Modifiers"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassPrivateCtor2.java"),
                                     expected);
    }

    @Test
    public void testPrivateClassWithDefaultCtor3() throws Exception {
        final String[] expected = {
            "26:5: " + getCheckMessage(MSG_KEY, "MyClass"),
            "30:5: " + getCheckMessage(MSG_KEY, "Check2"),
            "31:9: " + getCheckMessage(MSG_KEY, "Check3"),
            "35:5: " + getCheckMessage(MSG_KEY, "Check4"),
            "40:5: " + getCheckMessage(MSG_KEY, "Check"),
        };
        verifyWithInlineConfigParser(getPath("InputFinalClassPrivateCtor3.java"),
                                     expected);
    }
}
