import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock
from bot.lib import version_bump


class TestDecideBumpKind(unittest.TestCase):
    def test_few_files_few_lines_is_patch(self):
        self.assertEqual(version_bump.decide_bump_kind(1, 5), "patch")
        self.assertEqual(version_bump.decide_bump_kind(3, 50), "patch")

    def test_many_files_is_minor(self):
        self.assertEqual(version_bump.decide_bump_kind(4, 5), "minor")

    def test_many_lines_is_minor(self):
        self.assertEqual(version_bump.decide_bump_kind(1, 51), "minor")

    def test_both_over_threshold_is_minor(self):
        self.assertEqual(version_bump.decide_bump_kind(10, 200), "minor")


class TestVersionArithmetic(unittest.TestCase):
    def test_parse_version(self):
        self.assertEqual(version_bump.parse_version("5.5.7"), (5, 5, 7))

    def test_parse_version_invalid(self):
        with self.assertRaises(ValueError):
            version_bump.parse_version("garbage")

    def test_next_version_patch(self):
        self.assertEqual(version_bump.next_version("5.5.7", "patch"), "5.5.8")

    def test_next_version_minor_resets_patch(self):
        self.assertEqual(version_bump.next_version("5.5.7", "minor"), "5.6.0")

    def test_next_version_unknown_kind(self):
        with self.assertRaises(ValueError):
            version_bump.next_version("5.5.7", "major")


class TestReadCurrentVersion(unittest.TestCase):
    def test_reads_toc_version_line(self):
        repo = Path(tempfile.mkdtemp())
        (repo / "CritMatic.toc").write_text(
            "## Interface: 110207\n## Title: CritMatic\n## Version: 5.5.7\n## Author: x\n",
            encoding="utf-8",
        )
        self.assertEqual(version_bump.read_current_version(repo), "5.5.7")

    def test_raises_when_toc_missing(self):
        repo = Path(tempfile.mkdtemp())
        with self.assertRaises(FileNotFoundError):
            version_bump.read_current_version(repo)


class TestApplyBump(unittest.TestCase):
    def setUp(self):
        self.repo = Path(tempfile.mkdtemp())
        (self.repo / "CritMatic.toc").write_text(
            "## Interface: 110207\n## Version: 5.5.7\n## Author: x\n",
            encoding="utf-8",
        )
        (self.repo / "versions.json").write_text(
            json.dumps({"critmatic_version": "5.5.7"}, indent=2),
            encoding="utf-8",
        )
        (self.repo / "CHANGE_LOG.md").write_text(
            "# Changelog\n\n## [v5.5.7-release] - 05/06/2026\n- prior entry\n",
            encoding="utf-8",
        )

    def _fake_git_factory(self, tag_exists_initially=False):
        """Returns a side_effect for subprocess.run mocking git add/commit/tag/rev-parse calls."""
        state = {"tag_exists": set()}
        if tag_exists_initially:
            state["tag_exists"].add("v5.5.8")

        def run(cmd, *args, **kwargs):
            result = MagicMock(returncode=0, stdout="", stderr="")
            if cmd[0] == "git":
                if "rev-parse" in cmd and "--verify" in cmd:
                    tag = cmd[-1]
                    result.returncode = 0 if tag in state["tag_exists"] else 1
                elif "tag" in cmd and "-a" in cmd:
                    tag_idx = cmd.index("-a") + 1
                    state["tag_exists"].add(cmd[tag_idx])
                elif "tag" in cmd and "-d" in cmd:
                    tag_idx = cmd.index("-d") + 1
                    state["tag_exists"].discard(cmd[tag_idx])
            return result
        return run

    def test_rewrites_toc_versions_changelog(self):
        with patch("subprocess.run", side_effect=self._fake_git_factory()):
            tag = version_bump.apply_bump(
                repo_root=self.repo,
                new_version="5.5.8",
                changelog_body="Compatibility fix for WoW 12.0.5.",
                today_mmddyyyy="05/07/2026",
            )
        self.assertEqual(tag, "v5.5.8")
        toc = (self.repo / "CritMatic.toc").read_text(encoding="utf-8")
        self.assertIn("## Version: 5.5.8", toc)
        self.assertNotIn("## Version: 5.5.7", toc)
        versions = json.loads((self.repo / "versions.json").read_text(encoding="utf-8"))
        self.assertEqual(versions["critmatic_version"], "5.5.8")
        changelog = (self.repo / "CHANGE_LOG.md").read_text(encoding="utf-8")
        self.assertIn("## [v5.5.8-release] - 05/07/2026", changelog)
        self.assertIn("Compatibility fix for WoW 12.0.5.", changelog)
        self.assertIn("## [v5.5.7-release] - 05/06/2026", changelog)

    def test_tag_collision_appends_suffix(self):
        with patch("subprocess.run", side_effect=self._fake_git_factory(tag_exists_initially=True)):
            tag = version_bump.apply_bump(
                repo_root=self.repo,
                new_version="5.5.8",
                changelog_body="x",
                today_mmddyyyy="05/07/2026",
            )
        self.assertEqual(tag, "v5.5.8-1")

    def test_apply_bump_transactional_on_changelog_failure(self):
        # Simulate a failure mid-way: make CHANGE_LOG.md a directory so write fails.
        (self.repo / "CHANGE_LOG.md").unlink()
        (self.repo / "CHANGE_LOG.md").mkdir()
        before_toc = (self.repo / "CritMatic.toc").read_text(encoding="utf-8")
        before_versions = (self.repo / "versions.json").read_text(encoding="utf-8")
        with patch("subprocess.run", side_effect=self._fake_git_factory()):
            with self.assertRaises(Exception):
                version_bump.apply_bump(
                    repo_root=self.repo,
                    new_version="5.5.8",
                    changelog_body="x",
                    today_mmddyyyy="05/07/2026",
                )
        self.assertEqual((self.repo / "CritMatic.toc").read_text(encoding="utf-8"), before_toc)
        self.assertEqual((self.repo / "versions.json").read_text(encoding="utf-8"), before_versions)


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