import os
import sys
import shutil
import tempfile
import unittest
from textwrap import dedent

PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

from module_utils.role_dependency_resolver import RoleDependencyResolver  # noqa: E402


def write(path: str, content: str):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(dedent(content).lstrip())


def make_role(roles_dir: str, name: str):
    path = os.path.join(roles_dir, name)
    os.makedirs(path, exist_ok=True)
    os.makedirs(os.path.join(path, "tasks"), exist_ok=True)
    os.makedirs(os.path.join(path, "meta"), exist_ok=True)
    return path


class TestRoleDependencyResolver(unittest.TestCase):
    def setUp(self):
        self.roles_dir = tempfile.mkdtemp(prefix="roles_")

    def tearDown(self):
        shutil.rmtree(self.roles_dir, ignore_errors=True)

    # ----------------------------- TESTS -----------------------------

    def test_include_and_import_literal(self):
        """
        A/tasks/main.yml:
          - include_role: { name: B }
          - import_role:  { name: C }
        Expect: deps = {B, C}
        """
        make_role(self.roles_dir, "A")
        make_role(self.roles_dir, "B")
        make_role(self.roles_dir, "C")

        write(
            os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
            """
            - name: include B
              include_role:
                name: B

            - name: import C
              import_role:
                name: C
            """
        )

        r = RoleDependencyResolver(self.roles_dir)
        deps = r.get_role_dependencies("A")
        self.assertEqual(deps, {"B", "C"})

    def test_loop_with_string_items_and_dict_items(self):
        """
        A/tasks/main.yml uses loop with strings and dicts.
        Expect: {D, E, F, G}
        """
        make_role(self.roles_dir, "A")
        for rn in ["D", "E", "F", "G"]:
            make_role(self.roles_dir, rn)

        write(
            os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
            """
            - name: loop over strings → D, E
              include_role:
                name: "{{ item }}"
              loop:
                - D
                - E

            - name: loop over dicts → F, G
              import_role:
                name: "{{ item.role }}"
              with_items:
                - { role: "F" }
                - { role: "G" }
            """
        )

        r = RoleDependencyResolver(self.roles_dir)
        deps = r.get_role_dependencies("A")
        self.assertEqual(deps, {"D", "E", "F", "G"})

def test_jinja_mixed_name_glob_matching(self):
    """
    include_role:
      name: "prefix-{{ item }}-suffix"
      loop: [x, y]
    Existierende Rollen: prefix-x-suffix, prefix-y-suffix, prefix-z-suffix

    Erwartung:
      - KEINE Roh-Items ('x', 'y') als Rollen
      - Glob-Matching liefert die drei konkreten Rollen
    """
    make_role(self.roles_dir, "A")
    for rn in ["prefix-x-suffix", "prefix-y-suffix", "prefix-z-suffix"]:
        make_role(self.roles_dir, rn)

    write(
        os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
        """
        - name: jinja-mixed glob
          include_role:
            name: "prefix-{{ item }}-suffix"
          loop:
            - x
            - y
        """
    )

    r = RoleDependencyResolver(self.roles_dir)
    deps = r.get_role_dependencies("A")

    # keine Roh-Loop-Items
    self.assertNotIn("x", deps)
    self.assertNotIn("y", deps)

    # erwartete Rollen aus dem Glob-Matching
    self.assertEqual(
        deps,
        {"prefix-x-suffix", "prefix-y-suffix", "prefix-z-suffix"},
    )

    def test_pure_jinja_ignored_without_loop(self):
        """
        name: "{{ something }}" with no loop should be ignored.
        """
        make_role(self.roles_dir, "A")
        for rn in ["X", "Y"]:
            make_role(self.roles_dir, rn)

        write(
            os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
            """
            - name: pure var ignored
              include_role:
                name: "{{ something }}"
            """
        )

        r = RoleDependencyResolver(self.roles_dir)
        deps = r.get_role_dependencies("A")
        self.assertEqual(deps, set())

    def test_meta_dependencies_strings_and_dicts(self):
        """
        meta/main.yml:
          dependencies:
            - H
            - { role: I }
        Expect: {H, I}
        """
        make_role(self.roles_dir, "A")
        make_role(self.roles_dir, "H")
        make_role(self.roles_dir, "I")

        write(
            os.path.join(self.roles_dir, "A", "meta", "main.yml"),
            """
            ---
            dependencies:
              - H
              - { role: I }
            """
        )

        r = RoleDependencyResolver(self.roles_dir)
        deps = r.get_role_dependencies("A")
        self.assertEqual(deps, {"H", "I"})

    def test_run_after_extraction_and_toggle(self):
        """
        galaxy_info.run_after is only included when resolve_run_after=True
        """
        make_role(self.roles_dir, "A")
        make_role(self.roles_dir, "J")
        make_role(self.roles_dir, "K")

        write(
            os.path.join(self.roles_dir, "A", "meta", "main.yml"),
            """
            ---
            galaxy_info:
              run_after:
                - J
                - K
            dependencies: []
            """
        )

        r = RoleDependencyResolver(self.roles_dir)

        # Direkter Helper
        ra = r._extract_meta_run_after(os.path.join(self.roles_dir, "A"))
        self.assertEqual(ra, {"J", "K"})

        # Transitiv – off by default
        visited_off = r.resolve_transitively(["A"], resolve_run_after=False)
        self.assertNotIn("J", visited_off)
        self.assertNotIn("K", visited_off)

        # Transitiv – enabled
        visited_on = r.resolve_transitively(["A"], resolve_run_after=True)
        self.assertTrue({"A", "J", "K"}.issubset(visited_on))

    def test_cycle_and_max_depth(self):
        """
        A → include B
        B → import  A
        - Ensure cycle-safe traversal.
        - max_depth=0 → only start
        - max_depth=1 → start + direct deps
        """
        make_role(self.roles_dir, "A")
        make_role(self.roles_dir, "B")

        write(
            os.path.join(self.roles_dir, "A", "tasks", "main.yml"),
            """
            - include_role:
                name: B
            """
        )
        write(
            os.path.join(self.roles_dir, "B", "tasks", "main.yml"),
            """
            - import_role:
                name: A
            """
        )

        r = RoleDependencyResolver(self.roles_dir)

        visited = r.resolve_transitively(["A"])
        self.assertTrue({"A", "B"}.issubset(visited))

        only_start = r.resolve_transitively(["A"], max_depth=0)
        self.assertEqual(only_start, {"A"})

        depth_one = r.resolve_transitively(["A"], max_depth=1)
        self.assertEqual(depth_one, {"A", "B"})

    def test_tolerant_scan_fallback_on_invalid_yaml(self):
        """
        Force yaml.safe_load_all to fail and ensure tolerant scan picks up:
          - include_role literal name
          - loop list items
        """
        make_role(self.roles_dir, "A")
        for rn in ["R1", "R2", "R3"]:
            make_role(self.roles_dir, rn)

        # Invalid YAML (e.g., stray colon) to trigger exception
        write(
            os.path.join(self.roles_dir, "A", "tasks", "broken.yml"),
            """
            include_role:
              name: R1
            :: this line breaks YAML ::

            - include_role:
                name: "{{ item }}"
              loop:
                - R2
                - R3
            """
        )

        r = RoleDependencyResolver(self.roles_dir)
        inc, imp = r._scan_tasks(os.path.join(self.roles_dir, "A"))
        self.assertTrue({"R1", "R2", "R3"}.issubset(inc))
        self.assertEqual(imp, set())

    def test_resolve_transitively_combined_sources(self):
        """
        Combined test: include/import + dependencies (+ optional run_after).
        """
        for rn in ["ROOT", "C1", "C2", "D1", "D2", "RA1"]:
            make_role(self.roles_dir, rn)

        write(
            os.path.join(self.roles_dir, "ROOT", "tasks", "main.yml"),
            """
            - include_role: { name: C1 }
            - import_role:  { name: C2 }
            """
        )
        write(
            os.path.join(self.roles_dir, "ROOT", "meta", "main.yml"),
            """
            ---
            dependencies:
              - D1
              - { role: D2 }
            galaxy_info:
              run_after:
                - RA1
            """
        )

        r = RoleDependencyResolver(self.roles_dir)

        # Ohne run_after
        visited = r.resolve_transitively(["ROOT"], resolve_run_after=False)
        for expected in ["ROOT", "C1", "C2", "D1", "D2"]:
            self.assertIn(expected, visited)
        self.assertNotIn("RA1", visited)

        # Mit run_after
        visited_ra = r.resolve_transitively(["ROOT"], resolve_run_after=True)
        self.assertIn("RA1", visited_ra)


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