"""Property-based tests for the compare module functionality."""

from unittest.mock import ANY, MagicMock, patch

import pytest
from hypothesis import given, settings
from hypothesis import strategies as st

from recursivist.compare import (
    build_comparison_tree,
    compare_directory_structures,
    display_comparison,
    export_comparison,
)


@st.composite
def comparison_structure(draw, ensure_files=False):
    """Generate a structure for comparison testing."""
    structure = {}
    file_list = []
    if ensure_files:
        filename = "sample.txt"
        file_path = "/path/to/sample.txt"
        loc = 100
        size = 1024
        mtime = 1600000000.0
        file_list.append((filename, file_path, loc, size, mtime))
    for _ in range(draw(st.integers(min_value=0, max_value=5))):
        filename = draw(
            st.text(
                alphabet=st.characters(
                    whitelist_categories=("Lu", "Ll", "Nd"),
                    whitelist_characters="_-",
                ),
                min_size=1,
                max_size=20,
            )
        ) + draw(st.sampled_from([".txt", ".py", ".md", ".json", ".js"]))
        if draw(st.booleans()):
            file_path = "/path/to/" + filename
            loc = draw(st.integers(min_value=1, max_value=1000))
            size = draw(st.integers(min_value=1, max_value=10 * 1024 * 1024))
            mtime = draw(st.floats(min_value=1000000, max_value=1672531200))
            file_list.append((filename, file_path, loc, size, mtime))
        else:
            file_list.append(filename)
    structure["_files"] = file_list
    if draw(st.booleans()):
        structure["_loc"] = draw(st.integers(min_value=0, max_value=10000))
    if draw(st.booleans()):
        structure["_size"] = draw(st.integers(min_value=0, max_value=100 * 1024 * 1024))
    if draw(st.booleans()):
        structure["_mtime"] = draw(st.floats(min_value=1000000, max_value=1672531200))
    for _ in range(draw(st.integers(min_value=0, max_value=3))):
        dir_name = draw(
            st.text(
                alphabet=st.characters(
                    whitelist_categories=("Lu", "Ll", "Nd"),
                    whitelist_characters="_-",
                ),
                min_size=1,
                max_size=10,
            )
        )
        if draw(st.booleans()) and draw(st.booleans()):
            structure[dir_name] = {"_max_depth_reached": True}
        else:
            sub_structure = {}
            sub_file_list = []
            for _ in range(draw(st.integers(min_value=0, max_value=3))):
                sub_filename = draw(
                    st.text(
                        alphabet=st.characters(
                            whitelist_categories=("Lu", "Ll", "Nd"),
                            whitelist_characters="_-",
                        ),
                        min_size=1,
                        max_size=15,
                    )
                ) + draw(st.sampled_from([".txt", ".py", ".md"]))
                sub_file_list.append(sub_filename)
            sub_structure["_files"] = sub_file_list
            if draw(st.booleans()):
                sub_structure["_loc"] = draw(st.integers(min_value=0, max_value=5000))
            if draw(st.booleans()):
                sub_structure["_size"] = draw(
                    st.integers(min_value=0, max_value=50 * 1024 * 1024)
                )
            if draw(st.booleans()):
                sub_structure["_mtime"] = draw(
                    st.floats(min_value=1000000, max_value=1672531200)
                )
            structure[dir_name] = sub_structure
    return structure


@st.composite
def comparison_pair(draw, ensure_files=False):
    """Generate a pair of related structures for comparison testing."""
    base_structure = draw(comparison_structure(ensure_files=ensure_files))
    modified_structure = {}
    if "_files" in base_structure:
        modified_files = []
        for file_item in base_structure["_files"]:
            if draw(st.booleans()):
                modified_files.append(file_item)
        for _ in range(draw(st.integers(min_value=0, max_value=3))):
            new_filename = draw(
                st.text(
                    alphabet=st.characters(
                        whitelist_categories=("Lu", "Ll", "Nd"),
                        whitelist_characters="_-",
                    ),
                    min_size=1,
                    max_size=15,
                )
            ) + draw(st.sampled_from([".txt", ".py", ".md", ".json", ".js"]))
            if draw(st.booleans()):
                file_path = "/path/to/" + new_filename
                loc = draw(st.integers(min_value=1, max_value=1000))
                size = draw(st.integers(min_value=1, max_value=10 * 1024 * 1024))
                mtime = draw(st.floats(min_value=1000000, max_value=1672531200))
                modified_files.append((new_filename, file_path, loc, size, mtime))
            else:
                modified_files.append(new_filename)
        modified_structure["_files"] = modified_files
    if "_loc" in base_structure and draw(st.booleans()):
        modified_structure["_loc"] = base_structure["_loc"] + draw(
            st.integers(min_value=-100, max_value=100)
        )
    if "_size" in base_structure and draw(st.booleans()):
        modified_structure["_size"] = base_structure["_size"] + draw(
            st.integers(min_value=-1024, max_value=1024)
        )
    if "_mtime" in base_structure and draw(st.booleans()):
        modified_structure["_mtime"] = base_structure["_mtime"] + draw(
            st.floats(min_value=-86400, max_value=86400)
        )
    for key, value in base_structure.items():
        if key not in ["_files", "_loc", "_size", "_mtime", "_max_depth_reached"]:
            if draw(st.booleans()):
                modified_structure[key] = value
    for _ in range(draw(st.integers(min_value=0, max_value=2))):
        dir_name = draw(
            st.text(
                alphabet=st.characters(
                    whitelist_categories=("Lu", "Ll", "Nd"),
                    whitelist_characters="_-",
                ),
                min_size=1,
                max_size=10,
            )
        )
        if dir_name not in modified_structure:
            modified_structure[dir_name] = draw(comparison_structure())
    return (base_structure, modified_structure)


safe_path = st.text(
    alphabet=st.characters(
        blacklist_characters='\0\n\r\\/:*?"<>|',
    ),
    min_size=1,
    max_size=20,
)


class TestCompareDirectoryStructures:
    """Property-based tests for compare_directory_structures function."""

    @given(dir1=safe_path, dir2=safe_path)
    @settings(max_examples=10)
    def test_comparison_returns_structures(self, dir1, dir2):
        """Test that compare_directory_structures returns valid structures."""
        with patch("recursivist.compare.get_directory_structure") as mock_get_structure:
            mock_get_structure.side_effect = [
                ({"_files": ["file1.txt"]}, {".txt"}),
                ({"_files": ["file2.txt"]}, {".txt"}),
            ]
            structure1, structure2, extensions = compare_directory_structures(
                dir1, dir2
            )
            assert structure1 == {
                "_files": ["file1.txt"]
            }, "Should return structure1 from get_directory_structure"
            assert structure2 == {
                "_files": ["file2.txt"]
            }, "Should return structure2 from get_directory_structure"
            assert extensions == {".txt"}, "Should return combined extensions"
            assert (
                mock_get_structure.call_count == 2
            ), "get_directory_structure should be called twice"
            mock_get_structure.assert_any_call(
                dir1,
                None,
                None,
                None,
                exclude_patterns=None,
                include_patterns=None,
                max_depth=0,
                show_full_path=False,
                sort_by_loc=False,
                sort_by_size=False,
                sort_by_mtime=False,
            )
            mock_get_structure.assert_any_call(
                dir2,
                None,
                None,
                None,
                exclude_patterns=None,
                include_patterns=None,
                max_depth=0,
                show_full_path=False,
                sort_by_loc=False,
                sort_by_size=False,
                sort_by_mtime=False,
            )

    @given(dir1=safe_path, dir2=safe_path)
    @settings(max_examples=5)
    def test_comparison_with_options(self, dir1, dir2):
        """Test compare_directory_structures with various options."""
        with patch("recursivist.compare.get_directory_structure") as mock_get_structure:
            mock_get_structure.side_effect = [
                ({"_files": ["file1.txt"]}, {".txt"}),
                ({"_files": ["file2.txt"]}, {".txt"}),
            ]
            exclude_dirs = ["node_modules", "dist"]
            exclude_extensions = {".pyc", ".log"}
            exclude_patterns = [r"\.tmp$", r"^test_"]
            include_patterns = [r"\.py$"]
            max_depth = 2
            compare_directory_structures(
                dir1,
                dir2,
                exclude_dirs,
                ".gitignore",
                exclude_extensions,
                exclude_patterns=exclude_patterns,
                include_patterns=include_patterns,
                max_depth=max_depth,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )
            mock_get_structure.assert_any_call(
                dir1,
                exclude_dirs,
                ".gitignore",
                exclude_extensions,
                exclude_patterns=ANY,
                include_patterns=ANY,
                max_depth=max_depth,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )


class TestBuildComparisonTree:
    """Property-based tests for build_comparison_tree function."""

    @given(
        structures=comparison_pair(ensure_files=True),
        color_map=st.dictionaries(
            st.text(min_size=1, max_size=10),
            st.text(min_size=1, max_size=10),
            min_size=1,
            max_size=5,
        ),
    )
    @settings(max_examples=10)
    def test_build_comparison_tree(self, structures, color_map):
        """Test that build_comparison_tree builds a valid tree."""
        structure1, structure2 = structures
        mock_tree = MagicMock()
        mock_tree.add.return_value = mock_tree
        build_comparison_tree(structure1, structure2, mock_tree, color_map)
        assert mock_tree.add.call_count > 0, "Tree.add should have been called"

    @given(
        structures=comparison_pair(ensure_files=True),
        color_map=st.dictionaries(
            st.text(min_size=1, max_size=10),
            st.text(min_size=1, max_size=10),
            min_size=1,
            max_size=5,
        ),
    )
    @settings(max_examples=5)
    def test_build_comparison_tree_with_options(self, structures, color_map):
        """Test build_comparison_tree with various options."""
        structure1, structure2 = structures
        mock_tree = MagicMock()
        mock_tree.add.return_value = mock_tree
        build_comparison_tree(
            structure1,
            structure2,
            mock_tree,
            color_map,
            parent_name="root",
            show_full_path=True,
            sort_by_loc=True,
            sort_by_size=True,
            sort_by_mtime=True,
        )
        assert mock_tree.add.call_count > 0, "Tree.add should have been called"


class TestDisplayComparison:
    """Property-based tests for display_comparison function."""

    @given(dir1=safe_path, dir2=safe_path)
    @settings(max_examples=5)
    def test_display_comparison(self, dir1, dir2):
        """Test that display_comparison calls the necessary functions."""
        with patch(
            "recursivist.compare.compare_directory_structures"
        ) as mock_compare, patch(
            "recursivist.compare.generate_color_for_extension"
        ) as mock_color_map, patch(
            "recursivist.compare.Console"
        ) as mock_console, patch(
            "recursivist.compare.build_comparison_tree"
        ) as mock_build_tree, patch(
            "recursivist.compare.Tree"
        ) as mock_tree:
            mock_compare.return_value = ({"_files": []}, {"_files": []}, {".txt"})
            mock_color_map.return_value = "#FFFFFF"
            display_comparison(dir1, dir2)
            mock_compare.assert_called_once()
            mock_tree.assert_called()
            mock_build_tree.assert_called()
            mock_console.return_value.print.assert_called()

    @given(dir1=safe_path, dir2=safe_path)
    @settings(max_examples=5)
    def test_display_comparison_with_options(self, dir1, dir2):
        """Test display_comparison with various options."""
        with patch(
            "recursivist.compare.compare_directory_structures"
        ) as mock_compare, patch(
            "recursivist.compare.generate_color_for_extension"
        ) as mock_color_map:
            mock_compare.return_value = ({"_files": []}, {"_files": []}, {".txt"})
            mock_color_map.return_value = "#FFFFFF"
            display_comparison(
                dir1,
                dir2,
                exclude_dirs=["node_modules"],
                ignore_file=".gitignore",
                exclude_extensions={".log"},
                exclude_patterns=["test_*"],
                include_patterns=["*.py"],
                use_regex=True,
                max_depth=2,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )
            mock_compare.assert_called_once_with(
                dir1,
                dir2,
                ["node_modules"],
                ".gitignore",
                {".log"},
                exclude_patterns=ANY,
                include_patterns=ANY,
                max_depth=2,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )


class TestExportComparison:
    """Property-based tests for export_comparison function."""

    @given(
        dir1=safe_path,
        dir2=safe_path,
        output_path=safe_path,
    )
    @settings(max_examples=5)
    def test_export_comparison(self, dir1, dir2, output_path):
        """Test that export_comparison exports to HTML."""
        with patch(
            "recursivist.compare.compare_directory_structures"
        ) as mock_compare, patch(
            "recursivist.compare._export_comparison_to_html"
        ) as mock_export_html:
            mock_compare.return_value = ({"_files": []}, {"_files": []}, {".txt"})
            export_comparison(dir1, dir2, "html", output_path)
            mock_export_html.assert_called_once()

    @given(
        dir1=safe_path,
        dir2=safe_path,
        output_path=safe_path,
    )
    @settings(max_examples=5)
    def test_export_comparison_invalid_format(self, dir1, dir2, output_path):
        """Test that export_comparison raises an error for invalid formats."""
        with pytest.raises(ValueError) as excinfo:
            export_comparison(dir1, dir2, "invalid", output_path)
        assert "Only HTML format is supported" in str(excinfo.value)

    @given(
        dir1=safe_path,
        dir2=safe_path,
        output_path=safe_path,
    )
    @settings(max_examples=5)
    def test_export_comparison_with_options(self, dir1, dir2, output_path):
        """Test export_comparison with various options."""
        with patch(
            "recursivist.compare.compare_directory_structures"
        ) as mock_compare, patch("recursivist.compare._export_comparison_to_html") as _:
            mock_compare.return_value = ({"_files": []}, {"_files": []}, {".txt"})
            export_comparison(
                dir1,
                dir2,
                "html",
                output_path,
                exclude_dirs=["node_modules"],
                ignore_file=".gitignore",
                exclude_extensions={".log"},
                exclude_patterns=["test_*"],
                include_patterns=["*.py"],
                use_regex=True,
                max_depth=2,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )
            mock_compare.assert_called_once_with(
                dir1,
                dir2,
                ["node_modules"],
                ".gitignore",
                {".log"},
                exclude_patterns=ANY,
                include_patterns=ANY,
                max_depth=2,
                show_full_path=True,
                sort_by_loc=True,
                sort_by_size=True,
                sort_by_mtime=True,
            )


class TestExportComparisonToHTML:
    """Property-based tests for _export_comparison_to_html function."""

    @given(
        structures=comparison_pair(),
        dir1_name=safe_path,
        dir2_name=safe_path,
    )
    @settings(max_examples=5)
    def test_export_comparison_to_html(self, structures, dir1_name, dir2_name):
        """Test that _export_comparison_to_html creates a valid HTML file."""
        structure1, structure2 = structures
        comparison_data = {
            "dir1": {
                "path": f"/path/to/{dir1_name}",
                "name": dir1_name,
                "structure": structure1,
            },
            "dir2": {
                "path": f"/path/to/{dir2_name}",
                "name": dir2_name,
                "structure": structure2,
            },
            "metadata": {
                "exclude_patterns": [],
                "include_patterns": [],
                "pattern_type": "glob",
                "max_depth": 0,
                "show_full_path": False,
                "sort_by_loc": False,
                "sort_by_size": False,
                "sort_by_mtime": False,
            },
        }
        with patch("builtins.open", MagicMock()) as mock_open:
            mock_file = MagicMock()
            mock_open.return_value.__enter__.return_value = mock_file
            from recursivist.compare import _export_comparison_to_html

            with patch.object(mock_file, "write") as mock_write:
                _export_comparison_to_html(comparison_data, "output.html")
                mock_open.assert_called_once_with("output.html", "w", encoding="utf-8")
                assert mock_write.call_count > 0, "No data was written to the file"


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