"""Test cases for generating node-level dependencies (for fine-grained incremental checking)"""

from __future__ import annotations

import os
import sys
from collections import defaultdict

import pytest

from mypy import build
from mypy.errors import CompileError
from mypy.modulefinder import BuildSource
from mypy.nodes import Expression, MypyFile
from mypy.options import Options
from mypy.server.deps import get_dependencies
from mypy.test.config import test_temp_dir
from mypy.test.data import DataDrivenTestCase, DataSuite
from mypy.test.helpers import assert_string_arrays_equal, find_test_files, parse_options
from mypy.types import Type
from mypy.typestate import type_state

# Only dependencies in these modules are dumped
dumped_modules = ["__main__", "pkg", "pkg.mod"]


class GetDependenciesSuite(DataSuite):
    files = find_test_files(pattern="deps*.test")

    def run_case(self, testcase: DataDrivenTestCase) -> None:
        src = "\n".join(testcase.input)
        dump_all = "# __dump_all__" in src
        options = parse_options(src, testcase, incremental_step=1)
        if options.python_version > sys.version_info:
            pytest.skip("Test case requires a newer Python version")
        options.use_builtins_fixtures = True
        options.show_traceback = True
        options.cache_dir = os.devnull
        options.export_types = True
        options.preserve_asts = True
        options.allow_empty_bodies = True
        messages, files, type_map = self.build(src, options)
        a = messages
        if files is None or type_map is None:
            if not a:
                a = ["Unknown compile error (likely syntax error in test case or fixture)"]
        else:
            deps: defaultdict[str, set[str]] = defaultdict(set)
            for module, file in files.items():
                if (module in dumped_modules or dump_all) and (module in testcase.test_modules):
                    new_deps = get_dependencies(file, type_map, options.python_version, options)
                    for source in new_deps:
                        deps[source].update(new_deps[source])

            type_state.add_all_protocol_deps(deps)

            for source, targets in sorted(deps.items()):
                if source.startswith(("<enum", "<typing", "<mypy", "<_typeshed.")):
                    # Remove noise.
                    continue
                line = f"{source} -> {', '.join(sorted(targets))}"
                # Clean up output a bit
                line = line.replace("__main__", "m")
                a.append(line)

        assert_string_arrays_equal(
            testcase.output, a, f"Invalid output ({testcase.file}, line {testcase.line})"
        )

    def build(
        self, source: str, options: Options
    ) -> tuple[list[str], dict[str, MypyFile] | None, dict[Expression, Type] | None]:
        try:
            result = build.build(
                sources=[BuildSource("main", None, source)],
                options=options,
                alt_lib_path=test_temp_dir,
            )
        except CompileError as e:
            # TODO: Should perhaps not return None here.
            return e.messages, None, None
        return result.errors, result.files, result.types
