#!/usr/bin/env python3
from __future__ import annotations

import importlib.util
import pathlib
import sys
import tempfile
import unittest
from unittest import mock

SCRIPT_PATH = pathlib.Path(__file__).with_name(
    "generate_external_third_party.py")
SPEC = importlib.util.spec_from_file_location("generate_external_third_party",
                                              SCRIPT_PATH)
assert SPEC is not None
assert SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)


class GenerateExternalThirdPartyTest(unittest.TestCase):

    def test_script_has_no_lines_over_120_columns(self) -> None:
        script_path = pathlib.Path(MODULE.__file__)
        long_lines = [(line_number, len(line))
                      for line_number, line in enumerate(
                          script_path.read_text().splitlines(), 1)
                      if len(line) > 120]

        self.assertEqual(long_lines, [])

    def test_make_header_forwarder_renders_expected_include(self) -> None:
        self.assertEqual(
            MODULE.make_header_forwarder("opus/opus.h"),
            "#pragma once\n#include <opus/opus.h>\n",
        )

    def test_make_linked_source_set_build_gn_renders_expected_sections(
            self) -> None:
        text = MODULE.make_linked_source_set_build_gn(
            "sample",
            ["include", "{include_root}"],
            "samplelib",
            ["include/sample.h", "src/sample.cc"],
            public_config_name="sample_public_config",
            config_name="sample_config",
            deps=[":base"],
            extra_configs=["//build:no_chromium_code"],
            testonly=True,
        )

        self.assertIn('config("sample_public_config") {', text)
        self.assertIn('config("sample_config") {', text)
        self.assertIn('config("sample_link") {', text)
        self.assertIn('lib_dirs = [ "{lib_root}" ]', text)
        self.assertIn('testonly = true', text)
        self.assertIn('deps = [ ":base" ]', text)
        self.assertIn('"//build:no_chromium_code"', text)
        self.assertIn('"src/sample.cc",', text)

    def test_resolve_spec_normalizes_optional_fields(self) -> None:
        spec = MODULE.resolve_spec("libsrtp")

        self.assertEqual(spec.dep, "libsrtp")
        self.assertEqual(spec.relative_root, "third_party/libsrtp")
        self.assertEqual(spec.dirs, ("include", "srtp"))
        self.assertFalse(spec.preserve_root)
        self.assertEqual(spec.copy_files, {})
        self.assertIn("BUILD.gn", spec.files)

    def test_resolve_spec_rejects_missing_required_key(self) -> None:
        with self.assertRaisesRegex(ValueError, "relative_root"):
            MODULE.resolve_spec(
                "custom",
                specs={"custom": {
                    "dirs": [],
                    "files": {},
                }},
            )

    def test_resolve_spec_rejects_invalid_optional_type(self) -> None:
        with self.assertRaisesRegex(ValueError, "copy_files"):
            MODULE.resolve_spec(
                "custom",
                specs={
                    "custom": {
                        "relative_root": "third_party/custom",
                        "dirs": [],
                        "files": {},
                        "copy_files": [],
                    }
                },
            )

    def test_build_generation_plan_normalizes_output_and_files(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            spec = MODULE.resolve_spec("libsrtp")

            plan = MODULE.build_generation_plan(
                source_root,
                "/test/include",
                "/test/lib",
                "",
                spec,
            )

            self.assertEqual(plan.output_root,
                             source_root / "third_party" / "libsrtp")
            self.assertFalse(plan.preserve_root)
            self.assertIn(source_root / "third_party" / "libsrtp" / "include",
                          plan.directories)
            build_file = next(item for item in plan.files
                              if item.relative_path == "BUILD.gn")
            self.assertIn('include_dirs = [ "/test/include" ]',
                          build_file.content)
            self.assertIn('lib_dirs = [ "/test/lib" ]', build_file.content)

    def test_build_generation_plan_renders_tool_path(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            spec = MODULE.resolve_spec("nasm")

            plan = MODULE.build_generation_plan(
                source_root,
                "/test/include",
                "/test/lib",
                "/tool/nasm",
                spec,
            )

            build_gn = next(item for item in plan.files
                            if item.relative_path == "nasm_assemble.gni")
            self.assertIn('script = "/tool/nasm"', build_gn.content)
            self.assertIn('action_foreach(action_name) {', build_gn.content)
            self.assertNotIn('compiled_action_foreach(action_name)', build_gn.content)

    def test_build_generation_plan_renders_windows_tool_path(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            spec = MODULE.resolve_spec("nasm")

            plan = MODULE.build_generation_plan(
                source_root,
                "/test/include",
                "/test/lib",
                "/tool/nasm.exe",
                spec,
            )

            build_gn = next(item for item in plan.files
                            if item.relative_path == "nasm_assemble.gni")
            self.assertIn('script = "/tool/nasm.exe"', build_gn.content)
            self.assertNotIn('compiled_action_foreach(action_name)', build_gn.content)

    def test_build_generation_plan_preserves_root_flag(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            spec = MODULE.resolve_spec("third_party_root")

            plan = MODULE.build_generation_plan(
                source_root,
                "/test/include",
                "/test/lib",
                "",
                spec,
            )

            self.assertEqual(plan.output_root, source_root / "third_party")
            self.assertTrue(plan.preserve_root)
            self.assertEqual(plan.directories, ())

    def test_plan_generation_uses_resolved_spec(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            plan = MODULE.plan_generation(
                source_root,
                "testing",
                "/test/include",
                "/test/lib",
                "",
            )

            self.assertEqual(plan.output_root, source_root / "testing")
            self.assertIn(source_root / "testing" / "gmock", plan.directories)
            self.assertTrue(
                any(item.relative_path == "gtest/BUILD.gn"
                    for item in plan.files))

    def test_plan_generation_exposes_copied_files_tuple(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            plan = MODULE.plan_generation(
                source_root,
                "libsrtp",
                "/test/include",
                "/test/lib",
                "",
            )

            self.assertEqual(plan.copied_files, ())

    def test_apply_generation_plan_writes_planned_files(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            plan = MODULE.plan_generation(
                source_root,
                "libsrtp",
                "/test/include",
                "/test/lib",
                "",
            )

            MODULE.apply_generation_plan(plan)

            dep_root = source_root / "third_party" / "libsrtp"
            self.assertTrue((dep_root / "include").is_dir())
            self.assertIn('source_set("libsrtp")',
                          (dep_root / "BUILD.gn").read_text())

    def test_generate_external_dep_applies_plan(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            MODULE.generate_external_dep(
                source_root,
                "nasm",
                "/test/include",
                "/test/lib",
                "/tool/nasm",
            )

            dep_root = source_root / "third_party" / "nasm"
            self.assertIn('script = "/tool/nasm"',
                          (dep_root / "nasm_assemble.gni").read_text())

    def test_write_tree_generates_libsrtp_with_substitution(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            MODULE.write_tree(source_root, "libsrtp", "/test/include",
                              "/test/lib", "")

            dep_root = source_root / "third_party" / "libsrtp"
            self.assertTrue((dep_root / "include").is_dir())
            self.assertTrue((dep_root / "srtp").is_dir())
            build_gn = (dep_root / "BUILD.gn").read_text()
            self.assertIn('include_dirs = [ "/test/include" ]', build_gn)
            self.assertIn('lib_dirs = [ "/test/lib" ]', build_gn)
            self.assertIn('libs = [ "srtp2" ]', build_gn)
            header = (dep_root / "include" / "srtp.h").read_text()
            self.assertIn("#if __has_include(<srtp.h>)", header)

    def test_write_tree_preserves_existing_root_when_requested(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            dep_root = source_root / "third_party"
            dep_root.mkdir(parents=True, exist_ok=True)
            marker = dep_root / "keep.txt"
            marker.write_text("keep")

            MODULE.write_tree(source_root, "third_party_root", "/test/include",
                              "/test/lib", "")

            self.assertEqual(marker.read_text(), "keep")
            self.assertEqual((dep_root / "BUILD.gn").read_text(),
                             'group("jpeg") {\n}\n')

    def test_write_tree_renders_nasm_tool_path(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            MODULE.write_tree(source_root, "nasm", "/test/include",
                              "/test/lib", "/tool/nasm")

            dep_root = source_root / "third_party" / "nasm"
            assemble_gni = (dep_root / "nasm_assemble.gni").read_text()
            self.assertIn('script = "/tool/nasm"', assemble_gni)
            self.assertNotIn('compiled_action_foreach(action_name)', assemble_gni)
            self.assertEqual("", (dep_root / "BUILD.gn").read_text())

    def test_write_tree_generates_nested_testing_tree(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)

            MODULE.write_tree(source_root, "testing", "/test/include",
                              "/test/lib", "")

            dep_root = source_root / "testing"
            self.assertTrue((dep_root / "gmock").is_dir())
            self.assertTrue((dep_root / "gtest").is_dir())
            self.assertIn('group("pytype_dependencies")',
                          (dep_root / "BUILD.gn").read_text())
            self.assertIn('group("gtest")',
                          (dep_root / "gtest" / "BUILD.gn").read_text())
            self.assertIn('group("gmock")',
                          (dep_root / "gmock" / "BUILD.gn").read_text())

    def test_main_generates_representative_output_tree(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            source_root = pathlib.Path(tempdir)
            argv = [
                "generate_external_third_party.py",
                "--source-root",
                str(source_root),
                "--dep",
                "libsrtp",
                "--include-root",
                "/test/include",
                "--lib-root",
                "/test/lib",
            ]
            with mock.patch("sys.argv", argv):
                MODULE.main()

            dep_root = source_root / "third_party" / "libsrtp"
            self.assertTrue(dep_root.is_dir())
            self.assertTrue((dep_root / "include" / "srtp.h").is_file())
            self.assertIn('source_set("libsrtp")',
                          (dep_root / "BUILD.gn").read_text())

    def test_argparse_rejects_unknown_dep(self) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            argv = [
                "generate_external_third_party.py",
                "--source-root",
                tempdir,
                "--dep",
                "does-not-exist",
                "--include-root",
                "/test/include",
                "--lib-root",
                "/test/lib",
            ]
            with mock.patch("sys.argv", argv):
                with self.assertRaises(SystemExit):
                    MODULE.main()


if __name__ == "__main__":
    unittest.main()
