"""
Tests for conditions.py - Download conditions management.

This test suite provides comprehensive coverage for the conditions module:
- Tests Condition dataclass functionality
- Tests Conditions singleton class behavior
- Tests condition loading, saving, and validation
- Tests condition matching against info dicts
- Tests error handling and edge cases

Total test functions: 15+
All condition management functionality and edge cases are covered.
"""

import json
import tempfile
import uuid
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from app.library.conditions import Condition, Conditions


class TestCondition:
    """Test the Condition dataclass."""

    def test_condition_creation_with_defaults(self):
        """Test creating a condition with default values."""
        condition = Condition(name="test", filter="duration > 60")

        # Check that ID is generated
        assert condition.id
        assert isinstance(condition.id, str)

        # Check required fields
        assert condition.name == "test"
        assert condition.filter == "duration > 60"

        # Check defaults
        assert condition.cli == ""
        assert condition.extras == {}

    def test_condition_creation_with_all_fields(self):
        """Test creating a condition with all fields specified."""
        test_id = str(uuid.uuid4())
        extras = {"key": "value", "number": 42}

        condition = Condition(
            id=test_id,
            name="full_test",
            filter="uploader = 'test'",
            cli="--format best",
            extras=extras
        )

        assert condition.id == test_id
        assert condition.name == "full_test"
        assert condition.filter == "uploader = 'test'"
        assert condition.cli == "--format best"
        assert condition.extras == extras

    def test_condition_serialize(self):
        """Test condition serialization to dict."""
        condition = Condition(
            name="serialize_test",
            filter="title ~= 'test'",
            cli="--audio-quality 0",
            extras={"tag": "music"}
        )

        serialized = condition.serialize()

        assert isinstance(serialized, dict)
        assert serialized["name"] == "serialize_test"
        assert serialized["filter"] == "title ~= 'test'"
        assert serialized["cli"] == "--audio-quality 0"
        assert serialized["extras"] == {"tag": "music"}
        assert "id" in serialized

    def test_condition_json(self):
        """Test condition JSON serialization."""
        condition = Condition(name="json_test", filter="duration < 300")

        json_str = condition.json()

        assert isinstance(json_str, str)
        # Should be valid JSON
        data = json.loads(json_str)
        assert data["name"] == "json_test"
        assert data["filter"] == "duration < 300"

    def test_condition_get_method(self):
        """Test condition get method for accessing fields."""
        condition = Condition(
            name="get_test",
            filter="view_count > 1000",
            extras={"category": "popular"}
        )

        assert condition.get("name") == "get_test"
        assert condition.get("filter") == "view_count > 1000"
        assert condition.get("extras") == {"category": "popular"}
        assert condition.get("nonexistent") is None
        assert condition.get("nonexistent", "default") == "default"


class TestConditions:
    """Test the Conditions singleton class."""

    def setup_method(self):
        """Set up test fixtures by clearing singleton instances."""
        Conditions._reset_singleton()

    def test_conditions_singleton(self):
        """Test that Conditions follows singleton pattern."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "test_conditions.json"

            instance1 = Conditions(file=file_path)
            instance2 = Conditions.get_instance()

            assert instance1 is instance2

    def test_conditions_initialization(self):
        """Test Conditions initialization with file path."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "test_conditions.json"

            conditions = Conditions(file=file_path)

            assert conditions._file == file_path
            assert isinstance(conditions._items, list)

    @patch("app.library.conditions.Config.get_instance")
    def test_conditions_default_file_path(self, mock_config):
        """Test Conditions uses default config path when no file specified."""
        mock_config_instance = MagicMock()
        mock_config_instance.config_path = "/test/config"
        mock_config.return_value = mock_config_instance

        conditions = Conditions()

        expected_path = Path("/test/config") / "conditions.json"
        assert conditions._file == expected_path

    def test_get_all_empty(self):
        """Test get_all returns empty list when no conditions loaded."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "empty_conditions.json"
            conditions = Conditions(file=file_path)

            result = conditions.get_all()

            assert isinstance(result, list)
            assert len(result) == 0

    def test_clear(self):
        """Test clearing all conditions."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "clear_test.json"
            conditions = Conditions(file=file_path)

            # Add some test conditions
            conditions._items = [
                Condition(name="test1", filter="duration > 60"),
                Condition(name="test2", filter="uploader = 'test'")
            ]

            result = conditions.clear()

            assert result is conditions  # Should return self
            assert len(conditions._items) == 0

    def test_clear_when_already_empty(self):
        """Test clearing when conditions list is already empty."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "already_empty.json"
            conditions = Conditions(file=file_path)

            result = conditions.clear()

            assert result is conditions
            assert len(conditions._items) == 0

    def test_load_nonexistent_file(self):
        """Test loading from non-existent file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "nonexistent.json"
            conditions = Conditions(file=file_path)

            result = conditions.load()

            assert result is conditions
            assert len(conditions._items) == 0

    def test_load_empty_file(self):
        """Test loading from empty file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "empty.json"
            file_path.touch()  # Create empty file

            conditions = Conditions(file=file_path)
            result = conditions.load()

            assert result is conditions
            assert len(conditions._items) == 0

    def test_load_valid_conditions(self):
        """Test loading valid conditions from file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "valid_conditions.json"

            # Create test data
            test_data = [
                {
                    "id": str(uuid.uuid4()),
                    "name": "short_videos",
                    "filter": "duration < 300",
                    "cli": "--format worst",
                    "extras": {"category": "short"}
                },
                {
                    "id": str(uuid.uuid4()),
                    "name": "music_videos",
                    "filter": "title ~= 'music'",
                    "cli": "--audio-quality 0",
                    "extras": {"type": "audio"}
                }
            ]

            file_path.write_text(json.dumps(test_data, indent=4))

            conditions = Conditions(file=file_path)
            result = conditions.load()

            assert result is conditions
            assert len(conditions._items) == 2

            # Check first condition
            assert conditions._items[0].name == "short_videos"
            assert conditions._items[0].filter == "duration < 300"
            assert conditions._items[0].cli == "--format worst"
            assert conditions._items[0].extras == {"category": "short"}

            # Check second condition
            assert conditions._items[1].name == "music_videos"
            assert conditions._items[1].filter == "title ~= 'music'"

    def test_load_conditions_without_id(self):
        """Test loading conditions that don't have ID (should generate ID)."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_id_conditions.json"

            # Create test data without ID
            test_data = [
                {
                    "name": "no_id_test",
                    "filter": "duration > 120"
                }
            ]

            file_path.write_text(json.dumps(test_data))

            with patch.object(Conditions, "save") as mock_save:
                conditions = Conditions(file=file_path)
                conditions.load()

                # Should have generated ID
                assert len(conditions._items) == 1
                assert conditions._items[0].id
                assert conditions._items[0].name == "no_id_test"

                # Should call save due to changes
                mock_save.assert_called_once()

    def test_load_conditions_without_extras(self):
        """Test loading conditions that don't have extras field."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_extras_conditions.json"

            test_data = [
                {
                    "id": str(uuid.uuid4()),
                    "name": "no_extras_test",
                    "filter": "uploader = 'test'"
                }
            ]

            file_path.write_text(json.dumps(test_data))

            with patch.object(Conditions, "save") as mock_save:
                conditions = Conditions(file=file_path)
                conditions.load()

                # Should have generated empty extras
                assert len(conditions._items) == 1
                assert conditions._items[0].extras == {}

                # Should call save due to changes
                mock_save.assert_called_once()

    def test_load_invalid_json(self):
        """Test loading file with invalid JSON."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid.json"
            file_path.write_text("invalid json content")

            conditions = Conditions(file=file_path)
            result = conditions.load()

            assert result is conditions
            assert len(conditions._items) == 0

    def test_load_invalid_condition_data(self):
        """Test loading file with invalid condition data."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_data.json"

            # Missing required fields
            test_data = [
                {"id": "valid", "name": "valid", "filter": "duration > 60"},
                {"invalid": "data"}  # Missing required fields
            ]

            file_path.write_text(json.dumps(test_data))

            conditions = Conditions(file=file_path)
            result = conditions.load()

            # Should load only valid conditions
            assert result is conditions
            assert len(conditions._items) == 1
            assert conditions._items[0].name == "valid"

    def test_validate_valid_condition_dict(self):
        """Test validating a valid condition dictionary."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "validate_test.json"
            conditions = Conditions(file=file_path)

            valid_condition = {
                "id": str(uuid.uuid4()),
                "name": "valid_test",
                "filter": "duration > 60",
                "cli": "--format best",
                "extras": {"key": "value"}
            }

            result = conditions.validate(valid_condition)
            assert result is True

    def test_validate_valid_condition_object(self):
        """Test validating a valid Condition object."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "validate_obj_test.json"
            conditions = Conditions(file=file_path)

            valid_condition = Condition(
                name="valid_obj_test",
                filter="uploader = 'test'"
            )

            result = conditions.validate(valid_condition)
            assert result is True

    def test_validate_missing_id(self):
        """Test validating condition without ID."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_id_validate.json"
            conditions = Conditions(file=file_path)

            invalid_condition = {
                "name": "no_id_test",
                "filter": "duration > 60"
            }

            with pytest.raises(ValueError, match="No id found"):
                conditions.validate(invalid_condition)

    def test_validate_missing_name(self):
        """Test validating condition without name."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_name_validate.json"
            conditions = Conditions(file=file_path)

            invalid_condition = {
                "id": str(uuid.uuid4()),
                "filter": "duration > 60"
            }

            with pytest.raises(ValueError, match="No name found"):
                conditions.validate(invalid_condition)

    def test_validate_missing_filter(self):
        """Test validating condition without filter."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_filter_validate.json"
            conditions = Conditions(file=file_path)

            invalid_condition = {
                "id": str(uuid.uuid4()),
                "name": "no_filter_test"
            }

            with pytest.raises(ValueError, match="No filter found"):
                conditions.validate(invalid_condition)

    def test_validate_invalid_filter(self):
        """Test validating condition with invalid filter syntax."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_filter.json"
            conditions = Conditions(file=file_path)

            # Use a filter that will cause a syntax error in the parser
            invalid_condition = {
                "id": str(uuid.uuid4()),
                "name": "invalid_filter_test",
                "filter": "duration > & < 60",  # Invalid syntax with consecutive operators
                "cli": "",
                "extras": {}
            }

            with pytest.raises(ValueError, match="Invalid filter"):
                conditions.validate(invalid_condition)

    def test_validate_invalid_cli(self):
        """Test validating condition with invalid CLI options."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_cli.json"
            conditions = Conditions(file=file_path)

            invalid_condition = {
                "id": str(uuid.uuid4()),
                "name": "invalid_cli_test",
                "filter": "duration > 60",
                "cli": "--invalid-option-that-does-not-exist"
            }

            with pytest.raises(ValueError, match="Invalid command options"):
                conditions.validate(invalid_condition)

    def test_validate_invalid_extras_type(self):
        """Test validating condition with non-dict extras."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_extras.json"
            conditions = Conditions(file=file_path)

            invalid_condition = {
                "id": str(uuid.uuid4()),
                "name": "invalid_extras_test",
                "filter": "duration > 60",
                "extras": "not a dict"
            }

            with pytest.raises(ValueError, match="Extras must be a dictionary"):
                conditions.validate(invalid_condition)

    def test_validate_invalid_item_type(self):
        """Test validating invalid item type."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_type.json"
            conditions = Conditions(file=file_path)

            with pytest.raises(ValueError, match=r"Unexpected.*item type"):
                conditions.validate("invalid type")

    def test_save_conditions(self):
        """Test saving conditions to file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "save_test.json"
            conditions = Conditions(file=file_path)

            test_conditions = [
                Condition(name="save_test1", filter="duration > 60"),
                Condition(name="save_test2", filter="uploader = 'test'")
            ]

            result = conditions.save(test_conditions)

            assert result is conditions
            assert file_path.exists()

            # Verify file content
            saved_data = json.loads(file_path.read_text())
            assert len(saved_data) == 2
            assert saved_data[0]["name"] == "save_test1"
            assert saved_data[1]["name"] == "save_test2"

    def test_save_conditions_dict_format(self):
        """Test saving conditions in dictionary format."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "save_dict_test.json"
            conditions = Conditions(file=file_path)

            test_conditions = [
                {
                    "id": str(uuid.uuid4()),
                    "name": "dict_test",
                    "filter": "duration < 300",
                    "cli": "",
                    "extras": {}
                }
            ]

            conditions.save(test_conditions)

            assert file_path.exists()
            saved_data = json.loads(file_path.read_text())
            assert len(saved_data) == 1
            assert saved_data[0]["name"] == "dict_test"

    def test_has_condition_by_id(self):
        """Test checking if condition exists by ID."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "has_test.json"
            conditions = Conditions(file=file_path)

            test_id = str(uuid.uuid4())
            test_condition = Condition(id=test_id, name="has_test", filter="duration > 60")
            conditions._items = [test_condition]

            assert conditions.has(test_id) is True
            assert conditions.has("nonexistent") is False

    def test_has_condition_by_name(self):
        """Test checking if condition exists by name."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "has_name_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="has_name_test", filter="uploader = 'test'")
            conditions._items = [test_condition]

            assert conditions.has("has_name_test") is True
            assert conditions.has("nonexistent_name") is False

    def test_get_condition_by_id(self):
        """Test getting condition by ID."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "get_id_test.json"
            conditions = Conditions(file=file_path)

            test_id = str(uuid.uuid4())
            test_condition = Condition(id=test_id, name="get_id_test", filter="duration > 120")
            conditions._items = [test_condition]

            result = conditions.get(test_id)
            assert result is test_condition

            assert conditions.get("nonexistent") is None

    def test_get_condition_by_name(self):
        """Test getting condition by name."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "get_name_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="get_name_test", filter="title ~= 'music'")
            conditions._items = [test_condition]

            result = conditions.get("get_name_test")
            assert result is test_condition

    def test_get_condition_empty_id_or_name(self):
        """Test getting condition with empty ID or name."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "empty_get_test.json"
            conditions = Conditions(file=file_path)

            assert conditions.get("") is None
            assert conditions.get(None) is None

    @patch("app.library.conditions.match_str")
    def test_match_condition_found(self, mock_match_str):
        """Test matching condition against info dict."""
        mock_match_str.return_value = True

        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "match_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="match_test", filter="duration > 60")
            conditions._items = [test_condition]

            info_dict = {"duration": 120, "title": "Test Video"}
            result = conditions.match(info_dict)

            assert result is test_condition
            mock_match_str.assert_called_once_with("duration > 60", info_dict)

    @patch("app.library.conditions.match_str")
    def test_match_condition_not_found(self, mock_match_str):
        """Test matching when no condition matches."""
        mock_match_str.return_value = False

        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_match_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="no_match_test", filter="duration > 300")
            conditions._items = [test_condition]

            info_dict = {"duration": 60, "title": "Short Video"}
            result = conditions.match(info_dict)

            assert result is None

    def test_match_empty_conditions(self):
        """Test matching with empty conditions list."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "empty_match_test.json"
            conditions = Conditions(file=file_path)

            info_dict = {"duration": 120}
            result = conditions.match(info_dict)

            assert result is None

    def test_match_invalid_info_dict(self):
        """Test matching with invalid info dict."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_info_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="test", filter="duration > 60")
            conditions._items = [test_condition]

            # Test with None
            assert conditions.match(None) is None

            # Test with empty dict
            assert conditions.match({}) is None

            # Test with non-dict
            assert conditions.match("not a dict") is None

    @patch("app.library.conditions.match_str")
    def test_match_filter_evaluation_error(self, mock_match_str):
        """Test matching when filter evaluation raises exception."""
        mock_match_str.side_effect = Exception("Filter error")

        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "filter_error_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="error_test", filter="invalid filter")
            conditions._items = [test_condition]

            info_dict = {"duration": 120}
            result = conditions.match(info_dict)

            assert result is None

    def test_match_empty_filter(self):
        """Test matching with condition that has empty filter."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "empty_filter_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="empty_filter", filter="")
            conditions._items = [test_condition]

            info_dict = {"duration": 120}
            result = conditions.match(info_dict)

            assert result is None

    @patch("app.library.conditions.match_str")
    def test_single_match_found(self, mock_match_str):
        """Test single condition matching."""
        mock_match_str.return_value = True

        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "single_match_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="single_test", filter="uploader = 'test'")
            conditions._items = [test_condition]

            info_dict = {"uploader": "test", "title": "Test Video"}
            result = conditions.single_match("single_test", info_dict)

            assert result is test_condition
            mock_match_str.assert_called_once_with("uploader = 'test'", info_dict)

    @patch("app.library.conditions.match_str")
    def test_single_match_not_found(self, mock_match_str):
        """Test single condition matching when condition doesn't match."""
        mock_match_str.return_value = False

        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "single_no_match_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="single_no_match", filter="uploader = 'other'")
            conditions._items = [test_condition]

            info_dict = {"uploader": "test", "title": "Test Video"}
            result = conditions.single_match("single_no_match", info_dict)

            assert result is None

    def test_single_match_nonexistent_condition(self):
        """Test single matching with non-existent condition name."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "nonexistent_single_test.json"
            conditions = Conditions(file=file_path)

            info_dict = {"duration": 120}
            result = conditions.single_match("nonexistent", info_dict)

            assert result is None

    def test_single_match_condition_no_filter(self):
        """Test single matching with condition that has no filter."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "no_filter_single_test.json"
            conditions = Conditions(file=file_path)

            test_condition = Condition(name="no_filter_single", filter="")
            conditions._items = [test_condition]

            info_dict = {"duration": 120}
            result = conditions.single_match("no_filter_single", info_dict)

            assert result is None

    def test_single_match_invalid_inputs(self):
        """Test single matching with invalid inputs."""
        with tempfile.TemporaryDirectory() as temp_dir:
            file_path = Path(temp_dir) / "invalid_single_test.json"
            conditions = Conditions(file=file_path)

            # Test with empty conditions
            assert conditions.single_match("test", {"duration": 120}) is None

            # Test with None info
            assert conditions.single_match("test", None) is None

            # Test with empty info dict
            assert conditions.single_match("test", {}) is None

            # Test with non-dict info
            assert conditions.single_match("test", "not a dict") is None
