///////////////////////////////////////////////////////////////////////////////////////////////
// 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;

import static com.google.common.truth.Truth.assertWithMessage;
import static com.puppycrawl.tools.checkstyle.checks.coding.UnusedLocalVariableCheck.MSG_UNUSED_LOCAL_VARIABLE;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.DetailAstImpl;
import com.puppycrawl.tools.checkstyle.JavaParser;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.api.Violation;
import com.puppycrawl.tools.checkstyle.checks.coding.UnusedLocalVariableCheck;
import com.puppycrawl.tools.checkstyle.checks.naming.AbstractNameCheck;
import com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck;
import com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck;
import com.puppycrawl.tools.checkstyle.checks.sizes.ParameterNumberCheck;
import com.puppycrawl.tools.checkstyle.checks.whitespace.AbstractParenPadCheck;
import com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck;
import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;

public class SuppressWarningsHolderTest extends AbstractModuleTestSupport {

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

    @AfterEach
    public void cleanUp() {
        // clear cache that may have been set by tests

        new SuppressWarningsHolder().beginTree(null);

        final Map<String, String> map = TestUtil.getInternalStaticState(
                SuppressWarningsHolder.class, "CHECK_ALIAS_MAP");
        map.clear();
    }

    @Test
    public void testGet() {
        final SuppressWarningsHolder checkObj = new SuppressWarningsHolder();
        final int[] expected = {TokenTypes.ANNOTATION};
        assertWithMessage("Required token array differs from expected")
            .that(checkObj.getRequiredTokens())
            .isEqualTo(expected);
        assertWithMessage("Required token array differs from expected")
            .that(checkObj.getAcceptableTokens())
            .isEqualTo(expected);
    }

    @Test
    public void testOnComplexAnnotations() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder.java"), expected);
    }

    @Test
    public void testOnComplexAnnotationsNonConstant() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

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

    @Test
    public void testCustomAnnotation() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder5.java"), expected);
    }

    @Test
    public void testAll() throws Exception {
        final String[] expected = {
            "21:23: "
                    + getCheckMessage(TypecastParenPadCheck.class,
                            AbstractParenPadCheck.MSG_WS_NOT_PRECEDED, ")"),
        };

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder6.java"), expected);
    }

    @Test
    public void testGetDefaultAlias() {
        assertWithMessage("Default alias differs from expected")
            .that(SuppressWarningsHolder.getDefaultAlias("SomeName"))
            .isEqualTo("somename");
        assertWithMessage("Default alias differs from expected")
            .that(SuppressWarningsHolder.getDefaultAlias("SomeNameCheck"))
            .isEqualTo("somename");
    }

    @Test
    public void testSetAliasListEmpty() {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        holder.setAliasList("");
        assertWithMessage("Empty alias list should not be set")
            .that(SuppressWarningsHolder.getAlias(""))
            .isEqualTo("");
    }

    @Test
    public void testSetAliasListCorrect() {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        holder.setAliasList("alias=value");
        assertWithMessage("Alias differs from expected")
            .that(SuppressWarningsHolder.getAlias("alias"))
            .isEqualTo("value");
    }

    @Test
    public void testSetAliasListWrong() {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();

        try {
            holder.setAliasList("=SomeAlias");
            assertWithMessage("Exception expected").fail();
        }
        catch (IllegalArgumentException ex) {
            assertWithMessage("Error message is unexpected")
                .that(ex.getMessage())
                .isEqualTo("'=' expected in alias list item: =SomeAlias");
        }
    }

    @Test
    public void testIsSuppressed() throws Exception {
        populateHolder("MockEntry", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 100, 10);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isFalse();
    }

    @Test
    public void testIsSuppressedByName() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        final AuditEvent event = createAuditEvent("id", 110, 10);
        holder.setAliasList(MemberNameCheck.class.getName() + "=check");

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isTrue();
    }

    @Test
    public void testIsSuppressedByModuleId() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 350, 350);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isTrue();
    }

    @Test
    public void testIsSuppressedAfterEventEnd() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 350, 352);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isFalse();
    }

    @Test
    public void testIsSuppressedAfterEventEnd2() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 400, 10);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isFalse();
    }

    @Test
    public void testIsSuppressedAfterEventStart() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 100, 100);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isTrue();
    }

    @Test
    public void testIsSuppressedAfterEventStart2() throws Exception {
        populateHolder("check", 100, 100, 350, 350);
        final AuditEvent event = createAuditEvent("check", 100, 0);

        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(event))
                .isTrue();
    }

    @Test
    public void testIsSuppressedWithAllArgument() throws Exception {
        populateHolder("all", 100, 100, 350, 350);

        final Checker source = new Checker();
        final Violation firstViolationForTest =
            new Violation(100, 10, null, null, null, "id", MemberNameCheck.class, "msg");
        final AuditEvent firstEventForTest =
            new AuditEvent(source, "fileName", firstViolationForTest);
        assertWithMessage("Event is suppressed")
                .that(SuppressWarningsHolder.isSuppressed(firstEventForTest))
                .isFalse();

        final Violation secondViolationForTest =
            new Violation(100, 150, null, null, null, "id", MemberNameCheck.class, "msg");
        final AuditEvent secondEventForTest =
            new AuditEvent(source, "fileName", secondViolationForTest);
        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(secondEventForTest))
                .isTrue();

        final Violation thirdViolationForTest =
            new Violation(200, 1, null, null, null, "id", MemberNameCheck.class, "msg");
        final AuditEvent thirdEventForTest =
            new AuditEvent(source, "fileName", thirdViolationForTest);
        assertWithMessage("Event is not suppressed")
                .that(SuppressWarningsHolder.isSuppressed(thirdEventForTest))
                .isTrue();
    }

    @Test
    public void testAnnotationInTry() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder2.java"), expected);
    }

    @Test
    public void testEmptyAnnotation() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder3.java"), expected);
    }

    @Test
    public void testGetAllAnnotationValuesWrongArg() throws ReflectiveOperationException {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        final Method getAllAnnotationValues = holder.getClass()
                .getDeclaredMethod("getAllAnnotationValues", DetailAST.class);
        getAllAnnotationValues.setAccessible(true);

        final DetailAstImpl methodDef = new DetailAstImpl();
        methodDef.setType(TokenTypes.METHOD_DEF);
        methodDef.setText("Method Def");
        methodDef.setLineNo(0);
        methodDef.setColumnNo(0);

        final DetailAstImpl lparen = new DetailAstImpl();
        lparen.setType(TokenTypes.LPAREN);

        final DetailAstImpl parent = new DetailAstImpl();
        parent.addChild(lparen);
        parent.addChild(methodDef);

        try {
            getAllAnnotationValues.invoke(holder, parent);
            assertWithMessage("Exception expected").fail();
        }
        catch (ReflectiveOperationException ex) {
            assertWithMessage("Error type is unexpected")
                    .that(ex)
                    .hasCauseThat()
                    .isInstanceOf(IllegalArgumentException.class);
            assertWithMessage("Error message is unexpected")
                .that(ex)
                .hasCauseThat()
                .hasMessageThat()
                .isEqualTo("Unexpected AST: Method Def[0x0]");
        }
    }

    @Test
    public void testGetAnnotationValuesWrongArg() throws ReflectiveOperationException {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        final Method getAllAnnotationValues = holder.getClass()
                .getDeclaredMethod("getAnnotationValues", DetailAST.class);
        getAllAnnotationValues.setAccessible(true);

        final DetailAstImpl methodDef = new DetailAstImpl();
        methodDef.setType(TokenTypes.METHOD_DEF);
        methodDef.setText("Method Def");
        methodDef.setLineNo(0);
        methodDef.setColumnNo(0);

        try {
            getAllAnnotationValues.invoke(holder, methodDef);
            assertWithMessage("Exception expected").fail();
        }
        catch (ReflectiveOperationException ex) {
            assertWithMessage("Error type is unexpected")
                    .that(ex)
                    .hasCauseThat()
                    .isInstanceOf(IllegalArgumentException.class);
            assertWithMessage("Error message is unexpected")
                .that(ex)
                .hasCauseThat()
                .hasMessageThat()
                .isEqualTo("Expression or annotation array initializer AST expected: "
                        + "Method Def[0x0]");
        }
    }

    @Test
    public void testGetAnnotationTargetWrongArg() throws ReflectiveOperationException {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        final Method getAnnotationTarget = holder.getClass()
                .getDeclaredMethod("getAnnotationTarget", DetailAST.class);
        getAnnotationTarget.setAccessible(true);

        final DetailAstImpl methodDef = new DetailAstImpl();
        methodDef.setType(TokenTypes.METHOD_DEF);
        methodDef.setText("Method Def");

        final DetailAstImpl parent = new DetailAstImpl();
        parent.setType(TokenTypes.ASSIGN);
        parent.setText("Parent ast");
        parent.addChild(methodDef);
        parent.setLineNo(0);
        parent.setColumnNo(0);

        try {
            getAnnotationTarget.invoke(holder, methodDef);
            assertWithMessage("Exception expected").fail();
        }
        catch (ReflectiveOperationException ex) {
            assertWithMessage("Error type is unexpected")
                    .that(ex)
                    .hasCauseThat()
                    .isInstanceOf(IllegalArgumentException.class);
            assertWithMessage("Error message is unexpected")
                .that(ex)
                .hasCauseThat()
                .hasMessageThat()
                .isEqualTo("Unexpected container AST: Parent ast[0x0]");
        }
    }

    @Test
    public void testAstWithoutChildren() {
        final SuppressWarningsHolder holder = new SuppressWarningsHolder();
        final DetailAstImpl methodDef = new DetailAstImpl();
        methodDef.setType(TokenTypes.METHOD_DEF);

        try {
            holder.visitToken(methodDef);
            assertWithMessage("Exception expected").fail();
        }
        catch (IllegalArgumentException ex) {
            assertWithMessage("Error message is unexpected")
                .that(ex.getMessage())
                .isEqualTo("Identifier AST expected, but get null.");
        }
    }

    @Test
    public void testAnnotationWithFullName() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder4.java"), expected);
    }

    @Test
    public void testSuppressWarningsAsAnnotationProperty() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;

        verifyWithInlineConfigParser(getPath("InputSuppressWarningsHolder7.java"), expected);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testClearState() throws Exception {
        final SuppressWarningsHolder check = new SuppressWarningsHolder();

        final Optional<DetailAST> annotationDef = TestUtil.findTokenInAstByPredicate(
                JavaParser.parseFile(
                    new File(getPath("InputSuppressWarningsHolder.java")),
                    JavaParser.Options.WITHOUT_COMMENTS),
            ast -> ast.getType() == TokenTypes.ANNOTATION);

        assertWithMessage("Ast should contain ANNOTATION")
                .that(annotationDef.isPresent())
                .isTrue();
        assertWithMessage("State is not cleared on beginTree")
                .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, annotationDef.get(),
                        "ENTRIES",
                        entries -> ((ThreadLocal<List<Object>>) entries).get().isEmpty()))
                .isTrue();
    }

    private static void populateHolder(String checkName, int firstLine,
                                                         int firstColumn, int lastLine,
                                                         int lastColumn) throws Exception {
        final Class<?> entry = Class
                .forName("com.puppycrawl.tools.checkstyle.checks.SuppressWarningsHolder$Entry");
        final Constructor<?> entryConstr = entry.getDeclaredConstructor(String.class, int.class,
                int.class, int.class, int.class);
        entryConstr.setAccessible(true);

        final Object entryInstance = entryConstr.newInstance(checkName, firstLine,
                firstColumn, lastLine, lastColumn);

        final ThreadLocal<List<Object>> entries = TestUtil
                .getInternalStaticState(SuppressWarningsHolder.class, "ENTRIES");
        entries.get().add(entryInstance);
    }

    private static AuditEvent createAuditEvent(String moduleId, int line, int column) {
        final Checker source = new Checker();
        final Violation violation = new Violation(line, column, null, null, null,
                moduleId, MemberNameCheck.class, "violation");
        return new AuditEvent(source, "filename", violation);
    }

    @Test
    public void testSuppressWarningsTextBlocks() throws Exception {
        final String pattern = "^[a-z][a-zA-Z0-9]*$";

        final String[] expected = {
            "31:12: " + getCheckMessage(MemberNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "STRING3", pattern),
            "33:12: " + getCheckMessage(MemberNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "STRING4", pattern),
            "61:12: " + getCheckMessage(MemberNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "STRING8", pattern),
            };

        verifyWithInlineConfigParser(
                getNonCompilablePath("InputSuppressWarningsHolderTextBlocks.java"), expected);

    }

    @Test
    public void testWithAndWithoutCheckSuffixDifferentCases() throws Exception {
        final String pattern = "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$";
        final String[] expected = {
            "16:30: " + getCheckMessage(ConstantNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "a", pattern),
        };

        verifyWithInlineConfigParser(
                getPath("InputSuppressWarningsHolderWithAndWithoutCheckSuffixDifferentCases.java"),
                expected);
    }

    @Test
    public void testAliasList() throws Exception {
        final String[] expected = {
            "16:17: " + getCheckMessage(ParameterNumberCheck.class,
                    ParameterNumberCheck.MSG_KEY, 7, 8),
            "28:17: " + getCheckMessage(ParameterNumberCheck.class,
                    ParameterNumberCheck.MSG_KEY, 7, 8),
        };
        verifyWithInlineConfigParser(
                getPath("InputSuppressWarningsHolderAlias.java"),
                expected);
    }

    @Test
    public void testAliasList2() throws Exception {
        final String pattern = "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$";
        final String[] expected = {
            "16:29: " + getCheckMessage(ConstantNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "a", pattern),
            "19:30: " + getCheckMessage(ConstantNameCheck.class,
                AbstractNameCheck.MSG_INVALID_PATTERN, "b", pattern),
        };

        verifyWithInlineConfigParser(
                getPath("InputSuppressWarningsHolderAlias2.java"),
                expected);
    }

    @Test
    public void testIdent() throws Exception {
        final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
        verifyWithInlineConfigParser(
                getNonCompilablePath("InputSuppressWarningsHolder1.java"),
                expected);
    }

    @Test
    public void testIdent2() throws Exception {
        final String[] expected = {
            "37:9: " + getCheckMessage(UnusedLocalVariableCheck.class,
                    MSG_UNUSED_LOCAL_VARIABLE, "a"),
            "42:9: " + getCheckMessage(UnusedLocalVariableCheck.class,
                    MSG_UNUSED_LOCAL_VARIABLE, "a"),
        };
        verifyWithInlineConfigParser(
                getNonCompilablePath("InputSuppressWarningsHolder2.java"),
                expected);
    }
}
