"""Tests for release module just commands.

These tests verify that release commands work correctly.

IMPORTANT: Release commands are tested carefully to avoid:
- Pushing to PyPI/TestPyPI
- Pushing to Docker Hub
- Creating GitHub releases
- Creating unwanted git tags

Testing Strategy:
1. Syntax tests: Use --dry-run for all commands
2. Read-only tests: Safe commands like status, list-tags, latest-versions
3. Version bump tests: Test bump commands (they only modify local files)
4. Tag tests: Use TEST-RELEASE versions and clean up after
5. Skip push tests: Commands that push to remote are only syntax-tested

For integration testing of actual releases, use:
- TestPyPI with `-alpha` suffix
- Docker with `:test-release` tag
- Cleanup verification
"""

from __future__ import annotations

import subprocess
from typing import TYPE_CHECKING, ClassVar

import pytest

from tests.just_commands.conftest import PROJECT_ROOT

if TYPE_CHECKING:
    from collections.abc import Generator
    from pathlib import Path

    from tests.just_commands.conftest import JustRunner


class TestReleaseSyntax:
    """Syntax validation tests for release commands."""

    # All release commands for syntax testing
    RELEASE_COMMANDS: ClassVar[list[tuple[str, list[str]]]] = [
        # Version bump commands
        ("bump-workbench", ["0.0.1-test"]),
        ("bump-macro-magnets", ["0.0.1-test"]),
        ("bump-macro-export", ["0.0.1-test"]),
        # Tag commands (require version argument)
        ("tag-mcp-server", ["0.0.1-test"]),
        ("tag-workbench", ["0.0.1-test"]),
        ("tag-macro-magnets", ["0.0.1-test"]),
        ("tag-macro-export", ["0.0.1-test"]),
        # Info commands
        ("list-tags", []),
        ("latest-versions", []),
        ("status", []),
        ("changes-since", ["mcp-server"]),
        ("draft-notes", ["mcp-server"]),
        ("extract-changelog", ["mcp-server", "1.0.0"]),
        # Dry-run command
        ("dry-run-tag", ["mcp-server", "0.0.1-test"]),
        # Tag management
        ("delete-tag", ["test-tag-v0.0.1"]),
        # Wiki commands
        ("wiki-update", ["magnets"]),
        ("wiki-show", ["magnets"]),
        ("wiki-diff", ["magnets"]),
    ]

    @pytest.mark.just_syntax
    @pytest.mark.parametrize("command,args", RELEASE_COMMANDS)
    def test_release_command_syntax(
        self, just: JustRunner, command: str, args: list[str]
    ) -> None:
        """Release command should have valid syntax."""
        result = just.dry_run(f"release::{command}", *args)
        assert result.success, f"Syntax error in 'release::{command}': {result.stderr}"


class TestReleaseReadOnly:
    """Runtime tests for read-only release commands (safe to run)."""

    @pytest.mark.just_runtime
    def test_status_shows_release_info(self, just: JustRunner) -> None:
        """Status command should show release information."""
        result = just.run("release::status", timeout=30)
        assert result.success, f"Status failed: {result.stderr}"
        assert "Release Status" in result.stdout

    @pytest.mark.just_runtime
    def test_list_tags_works(self, just: JustRunner) -> None:
        """list-tags should show release tags."""
        result = just.run("release::list-tags", timeout=30)
        assert result.success, f"list-tags failed: {result.stderr}"
        # Should have section headers
        assert "MCP Server" in result.stdout or "Workbench" in result.stdout

    @pytest.mark.just_runtime
    def test_latest_versions_works(self, just: JustRunner) -> None:
        """latest-versions should show version information."""
        result = just.run("release::latest-versions", timeout=30)
        assert result.success, f"latest-versions failed: {result.stderr}"
        assert "Latest versions" in result.stdout

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "component",
        ["mcp-server", "workbench", "macro-magnets", "macro-export"],
    )
    def test_changes_since_works(self, just: JustRunner, component: str) -> None:
        """changes-since should work for each component."""
        result = just.run("release::changes-since", component, timeout=30)
        # Command should succeed (exit 0) even if there are no previous releases
        # The "No previous releases" message is informational, not an error
        assert result.success, (
            f"changes-since failed for {component}: "
            f"exit code {result.returncode}, output: {result.output}"
        )

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "component",
        ["mcp-server", "workbench", "macro-magnets", "macro-export"],
    )
    def test_draft_notes_works(self, just: JustRunner, component: str) -> None:
        """draft-notes should work for each component."""
        result = just.run("release::draft-notes", component, timeout=30)
        assert result.success, f"draft-notes failed: {result.stderr}"
        assert "Draft Release Notes" in result.stdout

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "component,version",
        [
            ("mcp-server", "1.0.0"),
            ("workbench", "1.0.0"),
            ("macro-magnets", "1.0.0"),
            ("macro-export", "1.0.0"),
        ],
    )
    def test_dry_run_tag_shows_info(
        self, just: JustRunner, component: str, version: str
    ) -> None:
        """dry-run-tag should show what would be created."""
        result = just.run("release::dry-run-tag", component, version, timeout=10)
        assert result.success, f"dry-run-tag failed: {result.stderr}"
        assert "Would create tag" in result.stdout

    @pytest.mark.just_runtime
    @pytest.mark.parametrize("macro", ["magnets", "export"])
    def test_wiki_show_works(self, just: JustRunner, macro: str) -> None:
        """wiki-show should display wiki source content."""
        result = just.run("release::wiki-show", macro, timeout=10)
        assert result.success, f"wiki-show failed: {result.stderr}"
        assert "Wiki Source" in result.stdout


class TestReleaseBumpCommands:
    """Tests for version bump commands.

    These commands modify local files but don't push anywhere.
    We test them but restore files afterward.
    """

    @pytest.fixture
    def backup_and_restore_files(
        self,
    ) -> Generator[None, None, None]:
        """Backup files before bump tests and restore after."""
        # Files that bump commands modify
        files_to_backup = [
            PROJECT_ROOT
            / "addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py",
            PROJECT_ROOT / "package.xml",
            PROJECT_ROOT / "macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro",
            PROJECT_ROOT
            / "macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md",
            PROJECT_ROOT / "macros/Cut_Object_for_Magnets/wiki-source.txt",
            PROJECT_ROOT / "macros/Multi_Export/MultiExport.FCMacro",
            PROJECT_ROOT / "macros/Multi_Export/README-MultiExport.md",
            PROJECT_ROOT / "macros/Multi_Export/wiki-source.txt",
        ]

        backups: dict[Path, str] = {}
        for file_path in files_to_backup:
            if file_path.exists():
                backups[file_path] = file_path.read_text(encoding="utf-8")

        yield

        # Restore all files
        for file_path, content in backups.items():
            file_path.write_text(content, encoding="utf-8")

    @pytest.mark.just_runtime
    @pytest.mark.just_release
    def test_bump_workbench_modifies_files(
        self, just: JustRunner, backup_and_restore_files: None
    ) -> None:
        """bump-workbench should modify version files."""
        result = just.run("release::bump-workbench", "99.99.99-test", timeout=30)
        assert result.success, f"bump-workbench failed: {result.stderr}"
        assert "Version bump complete" in result.stdout

        # Verify version was updated
        init_file = (
            PROJECT_ROOT / "addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py"
        )
        content = init_file.read_text()
        assert "99.99.99-test" in content

    @pytest.mark.just_runtime
    @pytest.mark.just_release
    def test_bump_macro_magnets_modifies_files(
        self, just: JustRunner, backup_and_restore_files: None
    ) -> None:
        """bump-macro-magnets should modify version files."""
        result = just.run("release::bump-macro-magnets", "99.99.99-test", timeout=30)
        assert result.success, f"bump-macro-magnets failed: {result.stderr}"
        assert "Version bump complete" in result.stdout

        # Verify version was updated
        macro_file = (
            PROJECT_ROOT / "macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro"
        )
        content = macro_file.read_text()
        assert "99.99.99-test" in content

    @pytest.mark.just_runtime
    @pytest.mark.just_release
    def test_bump_macro_export_modifies_files(
        self, just: JustRunner, backup_and_restore_files: None
    ) -> None:
        """bump-macro-export should modify version files."""
        result = just.run("release::bump-macro-export", "99.99.99-test", timeout=30)
        assert result.success, f"bump-macro-export failed: {result.stderr}"
        assert "Version bump complete" in result.stdout

        # Verify version was updated
        macro_file = PROJECT_ROOT / "macros/Multi_Export/MultiExport.FCMacro"
        content = macro_file.read_text()
        assert "99.99.99-test" in content


class TestReleaseTagCommands:
    """Tests for tag creation commands.

    IMPORTANT: These tests create and clean up test tags.
    They use special test versions to avoid conflicts with real releases.

    Tag naming: *-test-release-v0.0.0-test-XXXXXX
    Where XXXXXX is a random suffix to avoid conflicts.
    """

    @pytest.fixture
    def test_tag_prefix(self) -> str:
        """Generate a unique test tag prefix."""
        import random
        import string

        # S311: Random is fine for test fixture naming, not crypto
        suffix = "".join(
            random.choices(string.ascii_lowercase + string.digits, k=6)  # noqa: S311
        )
        return f"test-release-{suffix}"

    @pytest.mark.just_runtime
    @pytest.mark.just_release
    @pytest.mark.slow
    def test_tag_commands_require_clean_working_tree(self, just: JustRunner) -> None:
        """Tag commands should fail if working tree is dirty.

        We don't actually create tags here, just verify the validation.
        """
        # Create a temporary dirty file
        test_file = PROJECT_ROOT / ".test-dirty-file"
        try:
            test_file.write_text("test")
            # S603, S607: git is a well-known command, safe in test context
            subprocess.run(  # noqa: S603
                ["git", "add", str(test_file)],  # noqa: S607
                cwd=PROJECT_ROOT,
                capture_output=True,
                check=False,
            )

            # Tag commands should fail due to uncommitted changes
            # Use input to respond 'n' to confirmation prompt
            result = just.run(
                "release::tag-mcp-server",
                "0.0.1-test",
                timeout=10,
                input_text="n\n",
            )
            # Should fail before even asking for confirmation
            assert "uncommitted changes" in result.output.lower()

        finally:
            # Cleanup - S603, S607: git is a well-known command, safe in test context
            subprocess.run(  # noqa: S603
                ["git", "reset", "HEAD", str(test_file)],  # noqa: S607
                cwd=PROJECT_ROOT,
                capture_output=True,
                check=False,
            )
            test_file.unlink(missing_ok=True)

    @pytest.mark.just_runtime
    @pytest.mark.just_release
    def test_tag_commands_validate_version_format(self, just: JustRunner) -> None:
        """Tag commands should validate semver format."""
        # Invalid version format
        result = just.run(
            "release::tag-mcp-server",
            "invalid-version",
            timeout=10,
            input_text="n\n",
        )
        assert not result.success
        assert "not valid semver" in result.output.lower()


class TestReleaseValidation:
    """Tests to validate release command behavior without side effects."""

    @pytest.mark.just_runtime
    def test_delete_tag_requires_confirmation(self, just: JustRunner) -> None:
        """delete-tag should require confirmation."""
        result = just.run(
            "release::delete-tag",
            "nonexistent-tag-12345",
            timeout=10,
            input_text="n\n",
        )
        # Should abort when user says 'n'
        assert "Aborted" in result.output or "not found" in result.output.lower()

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "component",
        [
            "mcp-server",
            "server",
            "workbench",
            "macro-magnets",
            "magnets",
            "macro-export",
            "export",
        ],
    )
    def test_changes_since_component_aliases(
        self, just: JustRunner, component: str
    ) -> None:
        """changes-since should accept various component aliases."""
        result = just.run("release::changes-since", component, timeout=30)
        # Should not fail with "Unknown component"
        assert "Unknown component" not in result.output

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "version",
        [
            "1.0.0",
            "1.0.0-alpha",
            "1.0.0-alpha.1",
            "1.0.0-beta",
            "1.0.0-beta.2",
            "1.0.0-rc.1",
        ],
    )
    def test_dry_run_accepts_valid_versions(
        self, just: JustRunner, version: str
    ) -> None:
        """dry-run-tag should accept valid semver versions."""
        result = just.run("release::dry-run-tag", "mcp-server", version, timeout=10)
        assert result.success, f"dry-run-tag rejected valid version {version}"
        assert "Would create tag" in result.stdout

    @pytest.mark.just_runtime
    @pytest.mark.parametrize(
        "version",
        ["invalid", "1.0", "1", "v1.0.0", "1.0.0.0"],
    )
    def test_dry_run_rejects_invalid_versions(
        self, just: JustRunner, version: str
    ) -> None:
        """dry-run-tag should reject invalid versions."""
        result = just.run("release::dry-run-tag", "mcp-server", version, timeout=10)
        assert not result.success
        assert "Invalid version format" in result.output
