# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""Test json-based summary reporting for coverage.py"""

from __future__ import annotations

import copy
import json
import os

from datetime import datetime
from typing import Any

import coverage
from coverage import Coverage

from tests.coveragetest import UsingModulesMixin, CoverageTest


class JsonReportTest(UsingModulesMixin, CoverageTest):
    """Tests of the JSON reports from coverage.py."""

    def _assert_expected_json_report(
        self,
        cov: Coverage,
        expected_result: dict[str, Any],
    ) -> None:
        """
        Helper that creates an example file for most tests.
        """
        self.make_file("a.py", """\
            a = {'b': 1}
            if a.get('a'):
                b = 3
            elif a.get('b'):
                b = 5
            else:
                b = 7
            if not a:
                b = 9
            """)
        self._compare_json_reports(cov, expected_result, "a")

    def _assert_expected_json_report_with_regions(
        self,
        cov: Coverage,
        expected_result: dict[str, Any],
    ) -> None:
        """
        Helper that creates an example file for regions tests.
        """
        self.make_file("b.py", """\
            a = {"b": 1}

            def c():
                return 4

            class C:
                pass

            class D:
                def e(self):
                    if a.get("a"):
                        return 12
                    return 13
                def f(self):
                    return 15
            """)
        self._compare_json_reports(cov, expected_result, "b")

    def _compare_json_reports(
        self,
        cov: Coverage,
        expected_result: dict[str, Any],
        mod_name: str,
    ) -> None:
        """
        Helper that handles common ceremonies, comparing JSON reports that
        it creates to expected results, so tests can clearly show the
        consequences of setting various arguments.
        """
        mod = self.start_import_stop(cov, mod_name)
        output_path = os.path.join(self.temp_dir, f"{mod_name}.json")
        cov.json_report(mod, outfile=output_path)
        with open(output_path) as result_file:
            parsed_result = json.load(result_file)
        self.assert_recent_datetime(
            datetime.strptime(parsed_result['meta']['timestamp'], "%Y-%m-%dT%H:%M:%S.%f"),
        )
        del (parsed_result['meta']['timestamp'])
        expected_result["meta"].update({
            "version": coverage.__version__,
        })
        assert parsed_result == expected_result

    def test_branch_coverage(self) -> None:
        cov = coverage.Coverage(branch=True)
        a_py_result = {
            'executed_lines': [1, 2, 4, 5, 8],
            'missing_lines': [3, 7, 9],
            'excluded_lines': [],
            'executed_branches': [
                [2, 4],
                [4, 5],
                [8, -1],
            ],
            'missing_branches': [
                [2, 3],
                [4, 7],
                [8, 9],
            ],
            'summary': {
                'missing_lines': 3,
                'covered_lines': 5,
                'num_statements': 8,
                'num_branches': 6,
                'excluded_lines': 0,
                'num_partial_branches': 3,
                'covered_branches': 3,
                'missing_branches': 3,
                'percent_covered': 57.142857142857146,
                'percent_covered_display': '57',
            },
        }
        expected_result = {
            'meta': {
                "branch_coverage": True,
                "format": 3,
                "show_contexts": False,
            },
            'files': {
                'a.py': copy.deepcopy(a_py_result),
            },
            'totals': {
                'missing_lines': 3,
                'covered_lines': 5,
                'num_statements': 8,
                'num_branches': 6,
                'excluded_lines': 0,
                'num_partial_branches': 3,
                'percent_covered': 57.142857142857146,
                'percent_covered_display': '57',
                'covered_branches': 3,
                'missing_branches': 3,
            },
        }
        # With regions, a lot of data is duplicated.
        expected_result["files"]["a.py"]["classes"] = {"": a_py_result}     # type: ignore[index]
        expected_result["files"]["a.py"]["functions"] = {"": a_py_result}   # type: ignore[index]
        self._assert_expected_json_report(cov, expected_result)

    def test_simple_line_coverage(self) -> None:
        cov = coverage.Coverage()
        a_py_result = {
            'executed_lines': [1, 2, 4, 5, 8],
            'missing_lines': [3, 7, 9],
            'excluded_lines': [],
            'summary': {
                'excluded_lines': 0,
                'missing_lines': 3,
                'covered_lines': 5,
                'num_statements': 8,
                'percent_covered': 62.5,
                'percent_covered_display': '62',
            },
        }
        expected_result = {
            'meta': {
                "branch_coverage": False,
                "format": 3,
                "show_contexts": False,
            },
            'files': {
                'a.py': copy.deepcopy(a_py_result),
            },
            'totals': {
                'excluded_lines': 0,
                'missing_lines': 3,
                'covered_lines': 5,
                'num_statements': 8,
                'percent_covered': 62.5,
                'percent_covered_display': '62',
            },
        }
        # With regions, a lot of data is duplicated.
        expected_result["files"]["a.py"]["classes"] = {"": a_py_result}     # type: ignore[index]
        expected_result["files"]["a.py"]["functions"] = {"": a_py_result}   # type: ignore[index]
        self._assert_expected_json_report(cov, expected_result)

    def test_regions_coverage(self) -> None:
        cov = coverage.Coverage()
        expected_result = {
            "files": {
                "b.py": {
                    "classes": {
                        "": {
                            "excluded_lines": [],
                            "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                            "missing_lines": [4],
                            "summary": {
                                "covered_lines": 7,
                                "excluded_lines": 0,
                                "missing_lines": 1,
                                "num_statements": 8,
                                "percent_covered": 87.5,
                                "percent_covered_display": "88",
                            },
                        },
                        "C": {
                            "excluded_lines": [],
                            "executed_lines": [],
                            "missing_lines": [],
                            "summary": {
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_lines": 0,
                                "num_statements": 0,
                                "percent_covered": 100.0,
                                "percent_covered_display": "100",
                            },
                        },
                        "D": {
                            "executed_lines": [],
                            "excluded_lines": [],
                            "missing_lines": [11, 12, 13, 15],
                            "summary": {
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_lines": 4,
                                "num_statements": 4,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                    },
                    "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                    "excluded_lines": [],
                    "functions": {
                        "": {
                            "excluded_lines": [],
                            "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                            "missing_lines": [],
                            "summary": {
                                "covered_lines": 7,
                                "excluded_lines": 0,
                                "missing_lines": 0,
                                "num_statements": 7,
                                "percent_covered": 100.0,
                                "percent_covered_display": "100",
                            },
                        },
                        "c": {
                            "executed_lines": [],
                            "excluded_lines": [],
                            "missing_lines": [4],
                            "summary": {
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_lines": 1,
                                "num_statements": 1,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                        "D.e": {
                            "executed_lines": [],
                            "excluded_lines": [],
                            "missing_lines": [11, 12, 13],
                            "summary": {
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_lines": 3,
                                "num_statements": 3,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                        "D.f": {
                            "executed_lines": [],
                            "excluded_lines": [],
                            "missing_lines": [15],
                            "summary": {
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_lines": 1,
                                "num_statements": 1,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                    },
                    "missing_lines": [4, 11, 12, 13, 15],
                    "summary": {
                        "covered_lines": 7,
                        "excluded_lines": 0,
                        "missing_lines": 5,
                        "num_statements": 12,
                        "percent_covered": 58.333333333333336,
                        "percent_covered_display": "58",
                    },
                },
            },
            "meta": {
                "branch_coverage": False,
                "format": 3,
                "show_contexts": False,
            },
            "totals": {
                "covered_lines": 7,
                "excluded_lines": 0,
                "missing_lines": 5,
                "num_statements": 12,
                "percent_covered": 58.333333333333336,
                "percent_covered_display": "58",
            },
        }
        self._assert_expected_json_report_with_regions(cov, expected_result)

    def test_branch_regions_coverage(self) -> None:
        cov = coverage.Coverage(branch=True)
        expected_result = {
            "files": {
                "b.py": {
                    "classes": {
                        "": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                            "missing_branches": [],
                            "missing_lines": [4],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 7,
                                "excluded_lines": 0,
                                "missing_branches": 0,
                                "missing_lines": 1,
                                "num_branches": 0,
                                "num_partial_branches": 0,
                                "num_statements": 8,
                                "percent_covered": 87.5,
                                "percent_covered_display": "88",
                            },
                        },
                        "C": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [],
                            "missing_branches": [],
                            "missing_lines": [],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_branches": 0,
                                "missing_lines": 0,
                                "num_branches": 0,
                                "num_partial_branches": 0,
                                "num_statements": 0,
                                "percent_covered": 100.0,
                                "percent_covered_display": "100",
                            },
                        },
                        "D": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [],
                            "missing_branches": [[11, 12], [11, 13]],
                            "missing_lines": [11, 12, 13, 15],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_branches": 2,
                                "missing_lines": 4,
                                "num_branches": 2,
                                "num_partial_branches": 0,
                                "num_statements": 4,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                    },
                    "excluded_lines": [],
                    "executed_branches": [],
                    "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                    "functions": {
                        "": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [1, 3, 6, 7, 9, 10, 14],
                            "missing_branches": [],
                            "missing_lines": [],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 7,
                                "excluded_lines": 0,
                                "missing_branches": 0,
                                "missing_lines": 0,
                                "num_branches": 0,
                                "num_partial_branches": 0,
                                "num_statements": 7,
                                "percent_covered": 100.0,
                                "percent_covered_display": "100",
                            },
                        },
                        "D.e": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [],
                            "missing_branches": [[11, 12], [11, 13]],
                            "missing_lines": [11, 12, 13],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_branches": 2,
                                "missing_lines": 3,
                                "num_branches": 2,
                                "num_partial_branches": 0,
                                "num_statements": 3,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                        "D.f": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [],
                            "missing_branches": [],
                            "missing_lines": [15],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_branches": 0,
                                "missing_lines": 1,
                                "num_branches": 0,
                                "num_partial_branches": 0,
                                "num_statements": 1,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                        "c": {
                            "excluded_lines": [],
                            "executed_branches": [],
                            "executed_lines": [],
                            "missing_branches": [],
                            "missing_lines": [4],
                            "summary": {
                                "covered_branches": 0,
                                "covered_lines": 0,
                                "excluded_lines": 0,
                                "missing_branches": 0,
                                "missing_lines": 1,
                                "num_branches": 0,
                                "num_partial_branches": 0,
                                "num_statements": 1,
                                "percent_covered": 0.0,
                                "percent_covered_display": "0",
                            },
                        },
                    },
                    "missing_branches": [[11, 12], [11, 13]],
                    "missing_lines": [4, 11, 12, 13, 15],
                    "summary": {
                        "covered_branches": 0,
                        "covered_lines": 7,
                        "excluded_lines": 0,
                        "missing_branches": 2,
                        "missing_lines": 5,
                        "num_branches": 2,
                        "num_partial_branches": 0,
                        "num_statements": 12,
                        "percent_covered": 50.0,
                        "percent_covered_display": "50",
                    },
                },
            },
            "meta": {
                "branch_coverage": True,
                "format": 3,
                "show_contexts": False,
            },
            "totals": {
                "covered_branches": 0,
                "covered_lines": 7,
                "excluded_lines": 0,
                "missing_branches": 2,
                "missing_lines": 5,
                "num_branches": 2,
                "num_partial_branches": 0,
                "num_statements": 12,
                "percent_covered": 50.0,
                "percent_covered_display": "50",
            },
        }
        self._assert_expected_json_report_with_regions(cov, expected_result)

    def run_context_test(self, relative_files: bool) -> None:
        """A helper for two tests below."""
        self.make_file("config", f"""\
            [run]
            relative_files = {relative_files}

            [report]
            precision = 2

            [json]
            show_contexts = True
            """)
        cov = coverage.Coverage(context="cool_test", config_file="config")
        a_py_result = {
            "executed_lines": [1, 2, 4, 5, 8],
            "missing_lines": [3, 7, 9],
            "excluded_lines": [],
            "contexts": {
                "1": ["cool_test"],
                "2": ["cool_test"],
                "4": ["cool_test"],
                "5": ["cool_test"],
                "8": ["cool_test"],
            },
            "summary": {
                "excluded_lines": 0,
                "missing_lines": 3,
                "covered_lines": 5,
                "num_statements": 8,
                "percent_covered": 62.5,
                "percent_covered_display": "62.50",
            },
        }
        expected_result = {
            "meta": {
                "branch_coverage": False,
                "format": 3,
                "show_contexts": True,
            },
            "files": {
                "a.py": copy.deepcopy(a_py_result),
            },
            "totals": {
                "excluded_lines": 0,
                "missing_lines": 3,
                "covered_lines": 5,
                "num_statements": 8,
                "percent_covered": 62.5,
                "percent_covered_display": "62.50",
            },
        }
        # With regions, a lot of data is duplicated.
        expected_result["files"]["a.py"]["classes"] = {"": a_py_result}     # type: ignore[index]
        expected_result["files"]["a.py"]["functions"] = {"": a_py_result}   # type: ignore[index]
        self._assert_expected_json_report(cov, expected_result)

    def test_context_non_relative(self) -> None:
        self.run_context_test(relative_files=False)

    def test_context_relative(self) -> None:
        self.run_context_test(relative_files=True)

    def test_l1_equals_l2(self) -> None:
        # In results.py, we had a line checking `if l1 == l2` that was never
        # true.  This test makes it true. The annotations are essential, I
        # don't know why.
        self.make_file("wtf.py", """\
            def function(
                x: int,
                y: int,
            ) -> None:
                return x + y

            assert function(3, 5) == 8
            """)
        cov = coverage.Coverage(branch=True)
        mod = self.start_import_stop(cov, "wtf")
        cov.json_report(mod)
