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

import static com.google.common.truth.Truth.assertWithMessage;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import com.puppycrawl.tools.checkstyle.api.Violation;
import com.puppycrawl.tools.checkstyle.internal.utils.CloseAndFlushTestByteArrayOutputStream;

public class SarifLoggerTest extends AbstractModuleTestSupport {

    /**
     * Output stream to hold the test results. The IntelliJ IDEA issues the AutoCloseableResource
     * warning here, so it needs to be suppressed. The {@code ByteArrayOutputStream} does not hold
     * any resources that need to be released.
     */
    private final CloseAndFlushTestByteArrayOutputStream outStream =
        new CloseAndFlushTestByteArrayOutputStream();

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

    @Test
    public void testEscape() {
        final String[][] encodings = {
            {"\"", "\\\""},
            {"\\", "\\\\"},
            {"\b", "\\b"},
            {"\f", "\\f"},
            {"\n", "\\n"},
            {"\r", "\\r"},
            {"\t", "\\t"},
            {"/", "\\/"},
            {"\u0010", "\\u0010"},
            {"\u001E", "\\u001E"},
            {"\u001F", "\\u001F"},
            {" ", " "},
            {"bar1234", "bar1234"},
        };
        for (String[] encoding : encodings) {
            final String encoded = SarifLogger.escape(encoding[0]);
            assertWithMessage("\"" + encoding[0] + "\"")
                .that(encoded)
                .isEqualTo(encoding[1]);
        }
    }

    @Test
    public void testAddError() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
                new Violation(1, 1,
                        "messages.properties", "ruleId", null, SeverityLevel.ERROR, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
        logger.fileStarted(ev);
        logger.addError(ev);
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerSingleError.sarif"), outStream);
    }

    @Test
    public void testAddErrorWithWarningLevel() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
                new Violation(1, 1,
                        "messages.properties", "ruleId", null, SeverityLevel.WARNING, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
        logger.fileStarted(ev);
        logger.addError(ev);
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerSingleWarning.sarif"), outStream);
    }

    @Test
    public void testAddErrors() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
                new Violation(1, 1,
                        "messages.properties", "ruleId", null, SeverityLevel.INFO, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
        final Violation violation2 =
                new Violation(1, 1,
                        "messages.properties", "ruleId2", null, SeverityLevel.IGNORE, null,
                        getClass(), "found another error");
        final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
        logger.fileStarted(ev);
        logger.addError(ev);
        logger.fileFinished(ev);
        logger.fileStarted(ev2);
        logger.addError(ev2);
        logger.fileFinished(ev2);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerDoubleError.sarif"), outStream);
    }

    @Test
    public void testAddException() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation message =
                new Violation(1, 1,
                        "messages.properties", "null", null, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, null, message);
        logger.fileStarted(ev);
        logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerSingleException.sarif"), outStream);
    }

    @Test
    public void testAddExceptions() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
                new Violation(1, 1,
                        "messages.properties", "null", null, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, null, violation);
        final Violation violation2 =
                new Violation(1, 1,
                        "messages.properties", "null", null, null,
                        getClass(), "found an error");
        final AuditEvent ev2 = new AuditEvent(this, "Test.java", violation2);
        logger.fileStarted(ev);
        logger.addException(ev, new TestException("msg", new RuntimeException("msg")));
        logger.fileFinished(ev);
        logger.fileStarted(ev2);
        logger.addException(ev2, new TestException("msg2", new RuntimeException("msg2")));
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerDoubleException.sarif"), outStream);
    }

    @Test
    public void testLineOnly() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
            OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
            new Violation(1, 0,
                "messages.properties", "ruleId", null, null,
                getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, "Test.java", violation);
        logger.fileStarted(ev);
        logger.addError(ev);
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerLineOnly.sarif"), outStream);
    }

    @Test
    public void testEmpty() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        final Violation violation =
                new Violation(1, 1,
                        "messages.properties", "null", null, null,
                        getClass(), "found an error");
        final AuditEvent ev = new AuditEvent(this, null, violation);
        logger.fileStarted(ev);
        logger.fileFinished(ev);
        logger.auditFinished(null);
        verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
    }

    @Test
    public void testNullOutputStreamOptions() {
        try {
            final SarifLogger logger = new SarifLogger(outStream, null);
            // assert required to calm down eclipse's 'The allocated object is never used' violation
            assertWithMessage("Null instance")
                .that(logger)
                .isNotNull();
            assertWithMessage("Exception was expected").fail();
        }
        catch (IllegalArgumentException | IOException exception) {
            assertWithMessage("Invalid error message")
                .that(exception.getMessage())
                .isEqualTo("Parameter outputStreamOptions can not be null");
        }
    }

    @Test
    public void testCloseStream() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.auditStarted(null);
        logger.auditFinished(null);

        assertWithMessage("Invalid close count")
            .that(outStream.getCloseCount())
            .isEqualTo(1);

        verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
    }

    @Test
    public void testNoCloseStream() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.NONE);
        logger.auditStarted(null);
        logger.auditFinished(null);

        assertWithMessage("Invalid close count")
            .that(outStream.getCloseCount())
            .isEqualTo(0);
        assertWithMessage("Invalid flush count")
            .that(outStream.getFlushCount())
            .isEqualTo(1);

        outStream.close();
        verifyContent(getPath("ExpectedSarifLoggerEmpty.sarif"), outStream);
    }

    @Test
    public void testFinishLocalSetup() throws IOException {
        final SarifLogger logger = new SarifLogger(outStream,
                OutputStreamOptions.CLOSE);
        logger.finishLocalSetup();
        logger.auditStarted(null);
        logger.auditFinished(null);
        assertWithMessage("instance should not be null")
            .that(logger)
            .isNotNull();
    }

    @Test
    public void testReadResourceWithInvalidName() {
        try {
            SarifLogger.readResource("random");
            assertWithMessage("Exception expected").fail();
        }
        catch (IOException exception) {
            assertWithMessage("Exception message must match")
                .that(exception.getMessage())
                .isEqualTo("Cannot find the resource random");
        }
    }

    private static void verifyContent(
            String expectedOutputFile,
            ByteArrayOutputStream actualOutputStream) throws IOException {
        final String expectedContent = readFile(expectedOutputFile);
        final String actualContent =
                toLfLineEnding(actualOutputStream.toString(StandardCharsets.UTF_8));
        assertWithMessage("sarif content should match")
            .that(actualContent)
            .isEqualTo(expectedContent);
    }

    private static final class TestException extends RuntimeException {

        private static final long serialVersionUID = 1L;

        private TestException(String msg, Throwable cause) {
            super(msg, cause);
        }

        @Override
        public void printStackTrace(PrintWriter printWriter) {
            printWriter.print("stackTrace\nexample");
        }
    }
}
