"""Test suite for bumpversion module."""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest import mock

import pytest

from sfi.bumpversion.bumpversion import BumpversionManager, FileParser, Version, VersionPart


class TestVersion:
    """Test Version class functionality."""

    def test_parse_simple_version(self):
        """Test parsing a simple version string."""
        version = Version.parse("1.2.3")
        assert version.major == 1
        assert version.minor == 2
        assert version.patch == 3
        assert version.prerelease is None
        assert version.buildmetadata is None

    def test_parse_version_with_prerelease(self):
        """Test parsing version with prerelease tag."""
        version = Version.parse("1.2.3-alpha")
        assert version.major == 1
        assert version.minor == 2
        assert version.patch == 3
        assert version.prerelease == "alpha"
        assert version.buildmetadata is None

    def test_parse_version_with_build_metadata(self):
        """Test parsing version with build metadata."""
        version = Version.parse("1.2.3+build123")
        assert version.major == 1
        assert version.minor == 2
        assert version.patch == 3
        assert version.prerelease is None
        assert version.buildmetadata == "build123"

    def test_parse_version_with_prerelease_and_build_metadata(self):
        """Test parsing version with both prerelease and build metadata."""
        version = Version.parse("1.2.3-beta.2+build456")
        assert version.major == 1
        assert version.minor == 2
        assert version.patch == 3
        assert version.prerelease == "beta.2"
        assert version.buildmetadata == "build456"

    def test_parse_invalid_version(self):
        """Test parsing invalid version string raises ValueError."""
        try:
            Version.parse("invalid")
            pytest.fail("Should have raised ValueError")
        except ValueError as e:
            assert "Invalid version format" in str(e)

    def test_str_simple_version(self):
        """Test string representation of simple version."""
        version = Version(major=1, minor=2, patch=3)
        assert str(version) == "1.2.3"

    def test_str_version_with_prerelease(self):
        """Test string representation of version with prerelease."""
        version = Version(major=1, minor=2, patch=3, prerelease="alpha")
        assert str(version) == "1.2.3-alpha"

    def test_str_version_with_build_metadata(self):
        """Test string representation of version with build metadata."""
        version = Version(major=1, minor=2, patch=3, buildmetadata="build123")
        assert str(version) == "1.2.3+build123"

    def test_bump_major_version(self):
        """Test bumping major version."""
        version = Version(major=1, minor=2, patch=3)
        new_version = version.bump(VersionPart.MAJOR)
        assert new_version.major == 2
        assert new_version.minor == 0
        assert new_version.patch == 0
        assert new_version.prerelease is None
        assert new_version.buildmetadata is None

    def test_bump_minor_version(self):
        """Test bumping minor version."""
        version = Version(major=1, minor=2, patch=3)
        new_version = version.bump(VersionPart.MINOR)
        assert new_version.major == 1
        assert new_version.minor == 3
        assert new_version.patch == 0
        assert new_version.prerelease is None
        assert new_version.buildmetadata is None

    def test_bump_patch_version(self):
        """Test bumping patch version."""
        version = Version(major=1, minor=2, patch=3)
        new_version = version.bump(VersionPart.PATCH)
        assert new_version.major == 1
        assert new_version.minor == 2
        assert new_version.patch == 4
        assert new_version.prerelease is None
        assert new_version.buildmetadata is None

    def test_bump_preserves_prerelease(self):
        """Test bumping version preserves prerelease when reset_prerelease is False."""
        version = Version(major=1, minor=2, patch=3, prerelease="alpha")
        new_version = version.bump(VersionPart.PATCH, reset_prerelease=False)
        assert new_version.prerelease == "alpha"

    def test_bump_resets_prerelease_by_default(self):
        """Test bumping version resets prerelease by default."""
        version = Version(major=1, minor=2, patch=3, prerelease="alpha")
        new_version = version.bump(VersionPart.PATCH)
        assert new_version.prerelease is None

    def test_bump_resets_build_metadata(self):
        """Test bumping version always resets build metadata."""
        version = Version(major=1, minor=2, patch=3, buildmetadata="build123")
        new_version = version.bump(VersionPart.PATCH)
        assert new_version.buildmetadata is None

    def test_set_prerelease(self):
        """Test setting prerelease tag."""
        version = Version(major=1, minor=2, patch=3)
        new_version = version.set_prerelease("beta")
        assert new_version.prerelease == "beta"
        assert new_version.buildmetadata is None


class TestFileParser:
    """Test FileParser class functionality."""

    def test_parse_pyproject_toml(self):
        """Test parsing version from pyproject.toml."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[project]\nname = "test"\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            version, paths = FileParser.parse_pyproject(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
            assert paths == ["project.version"]
        finally:
            file_path.unlink()

    def test_parse_pyproject_toml_no_version(self):
        """Test parsing pyproject.toml without version raises ValueError."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[project]\nname = "test"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            try:
                FileParser.parse_pyproject(file_path)
                pytest.fail("Should have raised ValueError")
            except ValueError as e:
                assert "Version not found" in str(e)
        finally:
            file_path.unlink()

    def test_update_pyproject_toml(self):
        """Test updating version in pyproject.toml."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[project]\nname = "test"\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_pyproject(file_path, new_version)
            content = file_path.read_text()
            assert 'version = "2.0.0"' in content
        finally:
            file_path.unlink()

    def test_update_pyproject_toml_preserves_other_version_fields(self):
        """Test that updating version does not affect other fields containing 'version'."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write(
                "[project]\n"
                'name = "test"\n'
                'version = "1.2.3"\n'
                "\n"
                "[project.scripts]\n"
                'bumpversion = "sfi.bumpversion.bumpversion:main"\n'
                "\n"
                "[tool.ruff]\n"
                'target-version = "py38"\n'
                "\n"
                "[tool.uv.workspace]\n"
                'requires-python = ">=3.8"\n'
            )
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_pyproject(file_path, new_version)
            content = file_path.read_text()
            # Check that version was updated
            assert 'version = "2.0.0"' in content
            # Check that other fields containing 'version' were not modified
            assert 'bumpversion = "sfi.bumpversion.bumpversion:main"' in content
            assert 'target-version = "py38"' in content
        finally:
            file_path.unlink()

    def test_update_pyproject_toml_preserves_line_endings_unix(self):
        """Test that updating version preserves Unix line endings (LF)."""
        with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f:
            # Write with Unix line endings (LF)
            f.write(b'[project]\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_pyproject(file_path, new_version)
            # Read as binary to check actual line endings
            with file_path.open("rb") as f:
                content_bytes = f.read()
            # Check version was updated
            assert b'version = "2.0.0"' in content_bytes
            # Check line endings are still Unix (no \r\n)
            assert b"\r\n" not in content_bytes
            assert content_bytes.count(b"\n") > 0
        finally:
            file_path.unlink()

    def test_update_pyproject_toml_preserves_line_endings_windows(self):
        """Test that updating version preserves Windows line endings (CRLF)."""
        with tempfile.NamedTemporaryFile(mode="wb", suffix=".toml", delete=False) as f:
            # Write with Windows line endings (CRLF)
            f.write(b'[project]\r\nversion = "1.2.3"\r\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_pyproject(file_path, new_version)
            # Read as binary to check actual line endings
            with file_path.open("rb") as f:
                content_bytes = f.read()
            # Check version was updated
            assert b'version = "2.0.0"' in content_bytes
            # Check line endings are still Windows (has \r\n)
            assert b"\r\n" in content_bytes
            # Count should be the same as before (2 lines)
            assert content_bytes.count(b"\r\n") == 2
        finally:
            file_path.unlink()

    def test_parse_init_py(self):
        """Test parsing version from __init__.py."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write('__version__ = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            version, paths = FileParser.parse_init_py(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
            assert paths == []
        finally:
            file_path.unlink()

    def test_parse_init_py_no_version(self):
        """Test parsing __init__.py without version raises ValueError."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write("# No version here\n")
            f.flush()
            file_path = Path(f.name)

        try:
            try:
                FileParser.parse_init_py(file_path)
                pytest.fail("Should have raised ValueError")
            except ValueError as e:
                assert "Version not found" in str(e)
        finally:
            file_path.unlink()

    def test_update_init_py(self):
        """Test updating version in __init__.py."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write('__version__ = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_init_py(file_path, new_version)
            content = file_path.read_text()
            assert '__version__ = "2.0.0"' in content
        finally:
            file_path.unlink()

    def test_parse_setup_py(self):
        """Test parsing version from setup.py."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write('version = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            version, paths = FileParser.parse_setup_py(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
            assert paths == []
        finally:
            file_path.unlink()

    def test_update_setup_py(self):
        """Test updating version in setup.py."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
            f.write('version = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_setup_py(file_path, new_version)
            content = file_path.read_text()
            assert 'version = "2.0.0"' in content
        finally:
            file_path.unlink()

    def test_parse_package_json(self):
        """Test parsing version from package.json."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            f.write('{"name": "test", "version": "1.2.3"}\n')
            f.flush()
            file_path = Path(f.name)

        try:
            version, paths = FileParser.parse_package_json(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
            assert paths == []
        finally:
            file_path.unlink()

    def test_update_package_json(self):
        """Test updating version in package.json."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            f.write('{"name": "test", "version": "1.2.3"}\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_package_json(file_path, new_version)
            content = file_path.read_text()
            assert '"version": "2.0.0"' in content
        finally:
            file_path.unlink()

    def test_parse_cargo_toml(self):
        """Test parsing version from Cargo.toml."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[package]\nname = "test"\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            version, paths = FileParser.parse_cargo_toml(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
            assert paths == []
        finally:
            file_path.unlink()

    def test_update_cargo_toml(self):
        """Test updating version in Cargo.toml."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[package]\nname = "test"\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            new_version = Version(major=2, minor=0, patch=0)
            FileParser.update_cargo_toml(file_path, new_version)
            content = file_path.read_text()
            assert 'version = "2.0.0"' in content
        finally:
            file_path.unlink()


class TestBumpversionManager:
    """Test BumpversionManager class functionality."""

    def test_detect_files_pyproject_toml(self):
        """Test detecting pyproject.toml file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            files = manager.detect_files()

            assert len(files) == 1
            assert files[0] == pyproject

    def test_detect_files_multiple(self):
        """Test detecting multiple version files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')
            setup = tmpdir_path / "setup.py"
            setup.write_text('version = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            files = manager.detect_files()

            assert len(files) == 2
            assert pyproject in files
            assert setup in files

    def test_detect_files_none(self):
        """Test detecting version files when none exist."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)

            manager = BumpversionManager(tmpdir_path)
            files = manager.detect_files()

            assert len(files) == 0

    def test_parse_version_from_pyproject_toml(self):
        """Test parsing version from pyproject.toml file."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
            f.write('[project]\nversion = "1.2.3"\n')
            f.flush()
            file_path = Path(f.name)

        try:
            manager = BumpversionManager()
            version = manager.parse_version_from_file(file_path)
            assert version.major == 1
            assert version.minor == 2
            assert version.patch == 3
        finally:
            file_path.unlink()

    def test_parse_version_from_unsupported_file(self):
        """Test parsing version from unsupported file type raises ValueError."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
            f.write("version 1.2.3\n")
            f.flush()
            file_path = Path(f.name)

        try:
            manager = BumpversionManager()
            try:
                manager.parse_version_from_file(file_path)
                pytest.fail("Should have raised ValueError")
            except ValueError as e:
                assert "Unsupported file type" in str(e)
        finally:
            file_path.unlink()

    def test_bump_patch_version_single_file(self):
        """Test bumping patch version in a single file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH)

            assert str(new_version) == "1.2.4"
            content = pyproject.read_text()
            assert 'version = "1.2.4"' in content

    def test_bump_minor_version_single_file(self):
        """Test bumping minor version in a single file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.MINOR)

            assert str(new_version) == "1.3.0"
            content = pyproject.read_text()
            assert 'version = "1.3.0"' in content

    def test_bump_major_version_single_file(self):
        """Test bumping major version in a single file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.MAJOR)

            assert str(new_version) == "2.0.0"
            content = pyproject.read_text()
            assert 'version = "2.0.0"' in content

    def test_bump_version_with_prerelease(self):
        """Test bumping version with prerelease tag."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH, prerelease="alpha")

            assert str(new_version) == "1.2.3-alpha"
            content = pyproject.read_text()
            assert 'version = "1.2.3-alpha"' in content

    def test_bump_version_multiple_files(self):
        """Test bumping version in multiple files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')
            setup = tmpdir_path / "setup.py"
            setup.write_text('version = "1.2.3"\n')

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH)

            assert str(new_version) == "1.2.4"
            assert 'version = "1.2.4"' in pyproject.read_text()
            assert 'version = "1.2.4"' in setup.read_text()

    def test_bump_version_no_files(self):
        """Test bumping version when no files are available raises ValueError."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)

            manager = BumpversionManager(tmpdir_path)
            try:
                manager.bump(VersionPart.PATCH)
                pytest.fail("Should have raised ValueError")
            except ValueError as e:
                assert "No files to update" in str(e)

    @mock.patch("subprocess.run")
    def test_bump_version_with_git_commit(self, mock_run):
        """Test bumping version with git commit."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            # Mock git commands
            mock_run.side_effect = [
                None,  # git rev-parse
                None,  # git add
                None,  # git commit
            ]

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH, commit=True)

            assert str(new_version) == "1.2.4"
            assert mock_run.call_count == 3

    @mock.patch("subprocess.run")
    def test_bump_version_with_git_tag(self, mock_run):
        """Test bumping version with git tag."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            # Mock git commands
            mock_run.side_effect = [
                None,  # git rev-parse
                None,  # git add
                None,  # git commit
                None,  # git tag
            ]

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH, commit=True, tag=True)

            assert str(new_version) == "1.2.4"
            assert mock_run.call_count == 4

    @mock.patch("subprocess.run")
    def test_bump_version_not_git_repo(self, mock_run):
        """Test bumping version when not in a git repository."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.2.3"\n')

            # Mock git command to fail
            from subprocess import CalledProcessError

            mock_run.side_effect = CalledProcessError(1, "git")

            manager = BumpversionManager(tmpdir_path)
            new_version = manager.bump(VersionPart.PATCH, commit=True)

            # Should still bump version even though git operations failed
            assert str(new_version) == "1.2.4"
            assert 'version = "1.2.4"' in pyproject.read_text()

    def test_is_git_repo_true(self):
        """Test detecting a git repository returns True."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)

            manager = BumpversionManager(tmpdir_path)

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = None
                assert manager._is_git_repo() is True

    def test_is_git_repo_false(self):
        """Test detecting not a git repository returns False."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)

            manager = BumpversionManager(tmpdir_path)

            with mock.patch("subprocess.run") as mock_run:
                from subprocess import CalledProcessError

                mock_run.side_effect = CalledProcessError(1, "git")
                assert manager._is_git_repo() is False


class TestIntegration:
    """Integration tests for bumpversion."""

    def test_full_workflow_pyproject(self):
        """Test complete workflow with pyproject.toml."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nname = "test"\nversion = "1.0.0"\n')

            # First bump: patch
            manager = BumpversionManager(tmpdir_path)
            v1 = manager.bump(VersionPart.PATCH)
            assert str(v1) == "1.0.1"

            # Second bump: minor
            v2 = manager.bump(VersionPart.MINOR)
            assert str(v2) == "1.1.0"

            # Third bump: major
            v3 = manager.bump(VersionPart.MAJOR)
            assert str(v3) == "2.0.0"

    def test_full_workflow_multiple_files(self):
        """Test complete workflow with multiple files."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir_path = Path(tmpdir)
            pyproject = tmpdir_path / "pyproject.toml"
            pyproject.write_text('[project]\nversion = "1.0.0"\n')
            init_py = tmpdir_path / "__init__.py"
            init_py.write_text('__version__ = "1.0.0"\n')

            # Bump patch
            manager = BumpversionManager(tmpdir_path)
            v1 = manager.bump(VersionPart.PATCH)
            assert str(v1) == "1.0.1"

            # Check both files
            pyproject_content = pyproject.read_text()
            init_content = init_py.read_text()
            assert 'version = "1.0.1"' in pyproject_content
            assert '__version__ = "1.0.1"' in init_content

            # Add prerelease
            v2 = manager.bump(VersionPart.PATCH, prerelease="beta")
            assert str(v2) == "1.0.2-beta"

            # Check both files again
            pyproject_content = pyproject.read_text()
            init_content = init_py.read_text()
            assert 'version = "1.0.2-beta"' in pyproject_content
            assert '__version__ = "1.0.2-beta"' in init_content
