# pylint: disable=missing-function-docstring,line-too-long

"""Test for diff_cover.violationsreporters.java_violations_reporter"""

from io import BytesIO
from textwrap import dedent

import pytest

from diff_cover.command_runner import CommandError
from diff_cover.violationsreporters import base
from diff_cover.violationsreporters.base import QualityReporter
from diff_cover.violationsreporters.java_violations_reporter import (
    CheckstyleXmlDriver,
    FindbugsXmlDriver,
    PmdXmlDriver,
    Violation,
    checkstyle_driver,
)


@pytest.fixture(autouse=True)
def patch_so_all_files_exist(mocker):
    mock = mocker.patch.object(base.os.path, "exists")
    mock.returnvalue = True


@pytest.fixture
def process_patcher(mocker):
    def _inner(return_value, status_code=0):
        mocked_process = mocker.Mock()
        mocked_process.returncode = status_code
        mocked_process.communicate.return_value = return_value
        mocked_subprocess = mocker.patch("diff_cover.command_runner.subprocess")
        popen_mock = mocked_subprocess.Popen
        popen_instance = popen_mock.return_value
        popen_instance.__enter__ = mocker.Mock(return_value=mocked_process)
        popen_instance.__exit__ = mocker.Mock(return_value=None)
        return mocked_process

    return _inner


class TestCheckstyleQualityReporterTest:
    def test_no_such_file(self):
        """Expect that we get no results."""
        quality = QualityReporter(checkstyle_driver)

        result = quality.violations("")
        assert result == []

    def test_no_java_file(self):
        """Expect that we get no results because no Python files."""
        quality = QualityReporter(checkstyle_driver)
        file_paths = ["file1.coffee", "subdir/file2.js"]
        for path in file_paths:
            result = quality.violations(path)
            assert result == []

    def test_quality(self, process_patcher):
        """Integration test."""
        # Patch the output of `checkstyle`
        process_patcher(
            (
                dedent(
                    """
            [WARN] ../new_file.java:1:1: Line contains a tab character.
            [WARN] ../new_file.java:13: 'if' construct must use '{}'s.
            """
                )
                .strip()
                .encode("ascii"),
                "",
            )
        )

        expected_violations = [
            Violation(1, "Line contains a tab character."),
            Violation(13, "'if' construct must use '{}'s."),
        ]

        # Parse the report
        quality = QualityReporter(checkstyle_driver)

        # Expect that the name is set
        assert quality.name() == "checkstyle"

        # Measured_lines is undefined for a
        # quality reporter since all lines are measured
        assert not quality.measured_lines("../new_file.java")

        # Expect that we get violations for file1.java only
        # We're not guaranteed that the violations are returned
        # in any particular order.
        actual_violations = quality.violations("../new_file.java")
        assert len(actual_violations) == len(expected_violations)
        for expected in expected_violations:
            assert expected in actual_violations


class TestCheckstyleXmlQualityReporterTest:
    @pytest.fixture(autouse=True)
    def setup(self, mocker):
        # Paths generated by git_path are always the given argument
        _git_path_mock = mocker.patch(
            "diff_cover.violationsreporters.java_violations_reporter.GitPathTool"
        )
        _git_path_mock.relative_path = lambda path: path
        _git_path_mock.absolute_path = lambda path: path

    def test_no_such_file(self):
        quality = QualityReporter(CheckstyleXmlDriver())

        # Expect that we get no results
        result = quality.violations("")
        assert result == []

    def test_no_java_file(self):
        quality = QualityReporter(CheckstyleXmlDriver())
        file_paths = ["file1.coffee", "subdir/file2.js"]
        # Expect that we get no results because no Java files
        for path in file_paths:
            result = quality.violations(path)
            assert result == []

    def test_quality(self, process_patcher):
        # Patch the output of `checkstyle`
        process_patcher(
            (
                dedent(
                    """
            <?xml version="1.0" encoding="UTF-8"?>
            <checkstyle version="8.0">
                <file name="file1.java">
                    <error line="1" severity="error" message="Missing docstring"/>
                    <error line="2" severity="error" message="Unused variable 'd'"/>
                    <error line="2" severity="warning" message="TODO: Not the real way we'll store usages!"/>
                    <error line="579" severity="error" message="Unable to import 'rooted_paths'"/>
                    <error line="113" severity="error" message="Unused argument 'cls'"/>
                    <error line="150" severity="error" message="error while code parsing ([Errno 2] No such file or directory)"/>
                    <error line="149" severity="error" message="Comma not followed by a space"/>
                </file>
                <file name="path/to/file2.java">
                    <error line="100" severity="error" message="Access to a protected member"/>
                </file>
            </checkstyle>
            """
                )
                .strip()
                .encode("ascii"),
                "",
            )
        )

        expected_violations = [
            Violation(1, "error: Missing docstring"),
            Violation(2, "error: Unused variable 'd'"),
            Violation(2, "warning: TODO: Not the real way we'll store usages!"),
            Violation(579, "error: Unable to import 'rooted_paths'"),
            Violation(
                150,
                "error: error while code parsing ([Errno 2] No such file or directory)",
            ),
            Violation(149, "error: Comma not followed by a space"),
            Violation(113, "error: Unused argument 'cls'"),
        ]

        # Parse the report
        quality = QualityReporter(CheckstyleXmlDriver())

        # Expect that the name is set
        assert quality.name() == "checkstyle"

        # Measured_lines is undefined for a
        # quality reporter since all lines are measured
        assert not quality.measured_lines("file1.java")

        # Expect that we get violations for file1.java only
        # We're not guaranteed that the violations are returned
        # in any particular order.
        actual_violations = quality.violations("file1.java")
        assert len(actual_violations) == len(expected_violations)
        for expected in expected_violations:
            assert expected in actual_violations

    def test_quality_error(self, mocker, process_patcher):
        # Patch the output stderr/stdout and returncode of `checkstyle`
        process_patcher(
            (
                dedent(
                    """
            <?xml version="1.0" encoding="UTF-8"?>
            <checkstyle version="8.0">
                <file name="file1.java">
                    <error line="1" severity="error" message="Missing docstring"/>
                </file>
            </checkstyle>
            """
                ),
                b"oops",
            ),
            status_code=1,
        )

        # Parse the report
        code = mocker.patch(
            "diff_cover.violationsreporters.java_violations_reporter.run_command_for_code"
        )
        code.return_value = 0
        quality = QualityReporter(CheckstyleXmlDriver())

        with pytest.raises(CommandError):
            quality.violations("file1.java")

    def test_quality_pregenerated_report(self):
        # When the user provides us with a pre-generated checkstyle report
        # then use that instead of calling checkstyle directly.
        checkstyle_reports = [
            BytesIO(
                dedent(
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <checkstyle version="8.0">
                    <file name="path/to/file.java">
                        <error line="1" severity="error" message="Missing docstring"/>
                        <error line="57" severity="warning" message="TODO the name of this method is a little bit confusing"/>
                    </file>
                    <file name="another/file.java">
                        <error line="41" severity="error" message="Specify string format arguments as logging function parameters"/>
                        <error line="175" severity="error" message="Operator not preceded by a space"/>
                        <error line="259" severity="error" message="Invalid name '' for type variable (should match [a-z_][a-z0-9_]{2,30}$)"/>
                    </file>
                </checkstyle>
            """
                )
                .strip()
                .encode("utf-8")
            ),
            BytesIO(
                dedent(
                    """
            <?xml version="1.0" encoding="UTF-8"?>
            <checkstyle version="8.0">
                <file name="path/to/file.java">
                    <error line="183" severity="error" message="Invalid name '' for type argument (should match [a-z_][a-z0-9_]{2,30}$)"/>
                </file>
                <file name="another/file.java">
                    <error line="183" severity="error" message="Missing docstring"/>
                </file>
            </checkstyle>
            """
                )
                .strip()
                .encode("utf-8")
            ),
        ]

        # Generate the violation report
        quality = QualityReporter(CheckstyleXmlDriver(), reports=checkstyle_reports)

        # Expect that we get the right violations
        expected_violations = [
            Violation(1, "error: Missing docstring"),
            Violation(
                57, "warning: TODO the name of this method is a little bit confusing"
            ),
            Violation(
                183,
                "error: Invalid name '' for type argument (should match [a-z_][a-z0-9_]{2,30}$)",
            ),
        ]

        # We're not guaranteed that the violations are returned
        # in any particular order.
        actual_violations = quality.violations("path/to/file.java")
        assert len(actual_violations) == len(expected_violations)
        for expected in expected_violations:
            assert expected in actual_violations


class TestFindbugsQualityReporterTest:
    @pytest.fixture(autouse=True)
    def setup(self, mocker):
        # Paths generated by git_path are always the given argument
        _git_path_mock = mocker.patch(
            "diff_cover.violationsreporters.java_violations_reporter.GitPathTool"
        )
        _git_path_mock.relative_path = lambda path: path
        _git_path_mock.absolute_path = lambda path: path

    def test_no_such_file(self):
        quality = QualityReporter(FindbugsXmlDriver())

        # Expect that we get no results
        result = quality.violations("")
        assert result == []

    def test_no_java_file(self):
        quality = QualityReporter(FindbugsXmlDriver())
        file_paths = ["file1.coffee", "subdir/file2.js"]
        # Expect that we get no results because no Java files
        for path in file_paths:
            result = quality.violations(path)
            assert result == []

    def test_quality_pregenerated_report(self):
        # When the user provides us with a pre-generated findbugs report
        # then use that instead of calling findbugs directly.
        findbugs_reports = [
            BytesIO(
                dedent(
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <BugCollection sequence="0" release="" analysisTimestamp="1512755361404" version="3.0.1" timestamp="1512755226000">
                    <BugInstance instanceOccurrenceNum="0" instanceHash="1967bf8c4d25c6b964f30356014aa9fb" rank="20" abbrev="Dm" category="I18N" priority="3" type="DM_CONVERT_CASE" instanceOccurrenceMax="0">
                        <ShortMessage>Consider using Locale parameterized version of invoked method</ShortMessage>
                        <LongMessage>Use of non-localized String.toUpperCase() or String.toLowerCase() in org.opensource.sample.file$1.isMultipart(HttpServletRequest)</LongMessage>
                        <Class classname="org.opensource.sample.file$1" primary="true">
                            <SourceLine classname="org.opensource.sample.file$1" start="94" end="103" sourcepath="path/to/file.java" sourcefile="file.java">
                                <Message>At file.java:[lines 94-103]</Message>
                            </SourceLine>
                            <Message>In class org.opensource.sample.file$1</Message>
                        </Class>
                        <Method isStatic="false" classname="org.opensource.sample.file$1" signature="(Ljavax/servlet/http/HttpServletRequest;)Z" name="isMultipart" primary="true">
                            <SourceLine endBytecode="181" classname="org.opensource.sample.file$1" start="97" end="103" sourcepath="file1.java" sourcefile="file1.java" startBytecode="0" />
                            <Message>In method org.opensource.sample.file$1.isMultipart(HttpServletRequest)</Message>
                        </Method>
                        <SourceLine endBytecode="6" classname="org.opensource.sample.file$1" start="97" end="97" sourcepath="path/to/file.java" sourcefile="file.java" startBytecode="6" primary="true">
                            <Message>At file.java:[line 97]</Message>
                        </SourceLine>
                        <SourceLine role="SOURCE_LINE_ANOTHER_INSTANCE" endBytecode="55" classname="org.opensource.sample.file$1" start="103" end="104" sourcepath="another/file.java" sourcefile="file.java" startBytecode="55">
                            <Message>Another occurrence at file.java:[line 103, 104]</Message>
                        </SourceLine>
                    </BugInstance>
                </BugCollection>
            """
                )
                .strip()
                .encode("utf-8")
            ),
            BytesIO(
                dedent(
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <BugCollection sequence="0" release="" analysisTimestamp="1512755361404" version="3.0.1" timestamp="1512755226000">
                    <BugInstance instanceOccurrenceNum="0" instanceHash="1967bf8c4d25c6b964f30356014aa9fb" rank="20" abbrev="Dm" category="I18N" priority="3" type="DM_CONVERT_CASE" instanceOccurrenceMax="0">
                        <ShortMessage>Consider using Locale parameterized version of invoked method</ShortMessage>
                        <LongMessage>Use of non-localized String.toUpperCase() or String.toLowerCase() in org.opensource.sample.file$1.isMultipart(HttpServletRequest)</LongMessage>
                        <Class classname="org.opensource.sample.file$1" primary="true">
                            <SourceLine classname="org.opensource.sample.file$1" start="94" end="103" sourcepath="path/to/file.java" sourcefile="file.java">
                                <Message>At file.java:[lines 94-103]</Message>
                            </SourceLine>
                            <Message>In class org.opensource.sample.file$1</Message>
                        </Class>
                        <Method isStatic="false" classname="org.opensource.sample.file$1" signature="(Ljavax/servlet/http/HttpServletRequest;)Z" name="isMultipart" primary="true">
                            <SourceLine endBytecode="181" classname="org.opensource.sample.file$1" start="97" end="103" sourcepath="file1.java" sourcefile="file1.java" startBytecode="0" />
                            <Message>In method org.opensource.sample.file$1.isMultipart(HttpServletRequest)</Message>
                        </Method>
                        <SourceLine endBytecode="6" classname="org.opensource.sample.file$1" start="183" end="183" sourcepath="path/to/file.java" sourcefile="file.java" startBytecode="6" primary="true">
                            <Message>At file.java:[line 97]</Message>
                        </SourceLine>
                        <SourceLine role="SOURCE_LINE_ANOTHER_INSTANCE" endBytecode="55" classname="org.opensource.sample.file$1" start="183" end="183" sourcepath="another/file.java" sourcefile="file.java" startBytecode="55">
                            <Message>Another occurrence at file.java:[line 183]</Message>
                        </SourceLine>
                    </BugInstance>
                </BugCollection>
            """
                )
                .strip()
                .encode("utf-8")
            ),
            # this is a violation which is not bounded to a specific line. We'll skip those
            BytesIO(
                dedent(
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <BugCollection sequence="0" release="" analysisTimestamp="1512755361404" version="3.0.1" timestamp="1512755226000">
                    <BugInstance instanceOccurrenceNum="0" instanceHash="2820338ec68e2e75a81848c95d31167f" rank="19" abbrev="Se" category="BAD_PRACTICE" priority="3" type="SE_BAD_FIELD" instanceOccurrenceMax="0">
                        <ShortMessage>Non-transient non-serializable instance field in serializable class</ShortMessage>
                        <LongMessage>Class org.opensource.sample.file defines non-transient non-serializable instance field</LongMessage>
                        <SourceLine synthetic="true" classname="org.opensource.sample.file" sourcepath="path/to/file.java" sourcefile="file.java">
                            <Message>In file.java</Message>
                        </SourceLine>
                    </BugInstance>
                </BugCollection>
            """
                )
                .strip()
                .encode("utf-8")
            ),
        ]

        # Generate the violation report
        quality = QualityReporter(FindbugsXmlDriver(), reports=findbugs_reports)

        # Expect that we get the right violations
        expected_violations = [
            Violation(
                97,
                "I18N: Consider using Locale parameterized version of invoked method",
            ),
            Violation(
                183,
                "I18N: Consider using Locale parameterized version of invoked method",
            ),
        ]

        # We're not guaranteed that the violations are returned
        # in any particular order.
        actual_violations = quality.violations("path/to/file.java")
        assert len(actual_violations) == len(expected_violations)
        for expected in expected_violations:
            assert expected in actual_violations


class TestPmdXmlQualityReporterTest:
    @pytest.fixture(autouse=True)
    def setup(self, mocker):
        # Paths generated by git_path are always the given argument
        _git_path_mock = mocker.patch(
            "diff_cover.violationsreporters.java_violations_reporter.GitPathTool"
        )
        _git_path_mock.relative_path = lambda path: path
        _git_path_mock.absolute_path = lambda path: path

    def test_no_such_file(self):
        quality = QualityReporter(PmdXmlDriver())

        # Expect that we get no results
        result = quality.violations("")
        assert result == []

    def test_no_java_file(self):
        quality = QualityReporter(PmdXmlDriver())
        file_paths = ["file1.coffee", "subdir/file2.js"]
        # Expect that we get no results because no Java files
        for path in file_paths:
            result = quality.violations(path)
            assert result == []

    def test_quality_pregenerated_report(self):
        # When the user provides us with a pre-generated findbugs report
        # then use that instead of calling findbugs directly.
        pmd_reports = [
            BytesIO(
                dedent(
                    """
            <?xml version="1.0" encoding="UTF-8"?>
            <pmd version="5.6.1" timestamp="2019-06-24T15:47:13.429">
            <file name="path/to/file.java">
            <violation beginline="21" endline="118" begincolumn="8" endcolumn="1" rule="ClassMustHaveAuthorRule" ruleset="AlibabaJavaComments" package="com.huifu.devops.application.component" class="LingeringInputFilter" priority="3">
            must have @author comment
            </violation>
            </file>
            <file name="path/to/file.java">
            <violation beginline="10" endline="10" begincolumn="29" endcolumn="63" rule="AbstractMethodOrInterfaceMethodMustUseJavadocRule" ruleset="AlibabaJavaComments" package="com.huifu.devops.application.component" class="PipelineExecutionStepVoConverter" method="convert" priority="3">
            interface method must include javadoc comment
            </violation>
            </file>
            </pmd>
            """
                )
                .strip()
                .encode("utf-8")
            )
        ]

        pmd_xml_driver = PmdXmlDriver()
        # Generate the violation report
        quality = QualityReporter(pmd_xml_driver, reports=pmd_reports)

        # Expect that pmd is not installed
        assert not pmd_xml_driver.installed()

        # Expect that we get the right violations
        expected_violations = [
            Violation(21, "ClassMustHaveAuthorRule: must have @author comment"),
            Violation(
                10,
                "AbstractMethodOrInterfaceMethodMustUseJavadocRule: interface method must include javadoc comment",
            ),
        ]

        # We're not guaranteed that the violations are returned
        # in any particular order.
        actual_violations = quality.violations("path/to/file.java")
        assert len(actual_violations) == len(expected_violations)
        for expected in expected_violations:
            assert expected in actual_violations
