import importlib.util
import shutil
import sys
from pathlib import Path

MODULE_PATH = (
    Path(__file__).resolve().parents[2]
    / ".github"
    / "scripts"
    / "changelog_attribution.py"
)
SPEC = importlib.util.spec_from_file_location("changelog_attribution", MODULE_PATH)
changelog_attribution = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(changelog_attribution)


def make_component(tmp_path):
    component = tmp_path / "prowler"
    fragments_dir = component / "changelog.d"
    fragments_dir.mkdir(parents=True)
    return component, fragments_dir


def fake_git_mv(*args):
    if args[0] != "mv":
        raise AssertionError(f"Unexpected git command: {args}")
    shutil.move(args[1], args[2])
    return ""


def fail_git(*args):
    raise AssertionError(f"Unexpected git command: {args}")


def fail_api(*args):
    raise AssertionError(f"Unexpected GitHub API call: {args}")


def run_main(monkeypatch, component, *extra_args):
    monkeypatch.setattr(
        sys,
        "argv",
        ["changelog_attribution.py", str(component), *extra_args],
    )
    return changelog_attribution.main()


class TestChangelogAttribution:
    def test_renames_slug_fragment_to_resolved_pr_number(self, tmp_path, monkeypatch):
        component, fragments_dir = make_component(tmp_path)
        fragment = fragments_dir / "add-workflow.fixed.md"
        fragment.write_text("Fix changelog workflow.\n")

        monkeypatch.setattr(
            changelog_attribution, "find_adding_commit", lambda path: "abc123"
        )
        monkeypatch.setattr(
            changelog_attribution, "pr_from_api", lambda repo, sha: 11572
        )
        monkeypatch.setattr(changelog_attribution, "git", fake_git_mv)

        assert run_main(monkeypatch, component) == 0
        assert not fragment.exists()
        assert (fragments_dir / "11572.fixed.md").read_text() == (
            "Fix changelog workflow.\n"
        )

    def test_appends_counter_when_pr_fragment_already_exists(
        self, tmp_path, monkeypatch
    ):
        component, fragments_dir = make_component(tmp_path)
        (fragments_dir / "11572.fixed.md").write_text("Existing fix.\n")
        fragment = fragments_dir / "another-fix.fixed.md"
        fragment.write_text("Another fix.\n")

        monkeypatch.setattr(
            changelog_attribution, "find_adding_commit", lambda path: "abc123"
        )
        monkeypatch.setattr(
            changelog_attribution, "pr_from_api", lambda repo, sha: 11572
        )
        monkeypatch.setattr(changelog_attribution, "git", fake_git_mv)

        assert run_main(monkeypatch, component) == 0
        assert (fragments_dir / "11572.fixed.md").read_text() == "Existing fix.\n"
        assert (fragments_dir / "11572.fixed.1.md").read_text() == "Another fix.\n"

    def test_uses_subject_fallback_when_api_is_disabled(self, tmp_path, monkeypatch):
        component, fragments_dir = make_component(tmp_path)
        fragment = fragments_dir / "fallback.added.md"
        fragment.write_text("Add fallback behavior.\n")

        monkeypatch.setattr(
            changelog_attribution, "find_adding_commit", lambda path: "abc123"
        )
        monkeypatch.setattr(
            changelog_attribution,
            "pr_from_api",
            fail_api,
        )
        monkeypatch.setattr(changelog_attribution, "pr_from_subject", lambda sha: 42)
        monkeypatch.setattr(changelog_attribution, "git", fake_git_mv)

        assert run_main(monkeypatch, component, "--no-api") == 0
        assert not fragment.exists()
        assert (fragments_dir / "42.added.md").read_text() == "Add fallback behavior.\n"

    def test_renames_unresolved_fragment_to_orphan(self, tmp_path, monkeypatch, capsys):
        component, fragments_dir = make_component(tmp_path)
        fragment = fragments_dir / "manual.changed.md"
        fragment.write_text("Change manual entry.\n")

        monkeypatch.setattr(
            changelog_attribution, "find_adding_commit", lambda path: None
        )
        monkeypatch.setattr(changelog_attribution, "git", fake_git_mv)

        assert run_main(monkeypatch, component) == 0
        assert not fragment.exists()
        assert (fragments_dir / "+manual.changed.md").read_text() == (
            "Change manual entry.\n"
        )
        assert "Could not resolve a PR" in capsys.readouterr().out

    def test_rejects_malformed_fragment_names(self, tmp_path, monkeypatch, capsys):
        component, fragments_dir = make_component(tmp_path)
        fragment = fragments_dir / "bad.bugfix.md"
        fragment.write_text("Invalid type.\n")

        monkeypatch.setattr(
            changelog_attribution,
            "git",
            fail_git,
        )

        assert run_main(monkeypatch, component) == 1
        assert fragment.exists()
        assert "Malformed fragment filename" in capsys.readouterr().out

    def test_rejects_malformed_names_before_renaming_any_fragment(
        self, tmp_path, monkeypatch, capsys
    ):
        component, fragments_dir = make_component(tmp_path)
        valid_fragment = fragments_dir / "valid.fixed.md"
        malformed_fragment = fragments_dir / "bad.bugfix.md"
        valid_fragment.write_text("Valid fix.\n")
        malformed_fragment.write_text("Invalid type.\n")

        monkeypatch.setattr(
            changelog_attribution, "find_adding_commit", lambda path: "abc123"
        )
        monkeypatch.setattr(
            changelog_attribution, "pr_from_api", lambda repo, sha: 11572
        )
        monkeypatch.setattr(changelog_attribution, "git", fail_git)

        assert run_main(monkeypatch, component) == 1
        assert valid_fragment.read_text() == "Valid fix.\n"
        assert malformed_fragment.read_text() == "Invalid type.\n"
        assert not (fragments_dir / "11572.fixed.md").exists()
        assert "Malformed fragment filename" in capsys.readouterr().out

    def test_skips_fragments_that_already_start_with_pr_number(
        self, tmp_path, monkeypatch
    ):
        component, fragments_dir = make_component(tmp_path)
        fragment = fragments_dir / "11572.added.md"
        fragment.write_text("Already attributed.\n")

        monkeypatch.setattr(
            changelog_attribution,
            "git",
            fail_git,
        )

        assert run_main(monkeypatch, component) == 0
        assert fragment.read_text() == "Already attributed.\n"
