from unittest.mock import MagicMock
from uuid import uuid4

from dsg_core.domain.items.commands import RefreshItemCommand
from dsg_core.domain.validation import ValidationError
from dsg_core.projections.events import ItemProjectionRefreshedEvent
from dsg_core.services.data.commandDryrunService import CommandDryrunService
from dsg_core.services.data import dataCommandHandler as data_command_handler_module
from dsg_core.services.data.dataCommandHandler import (
    DataCommandHandler,
    NESTED_COMMAND_VALIDATION_ERROR_LIMIT,
)
from dsg_core.services.data.dependencies import RefsAndDepsSchema
from dsg_core.services.data.dataCommandProcessor import (
    DataCommandProcessor,
    DataCommandProcessorOutput,
)


def test_data_command_processor_forwards_skip_nested_commands() -> None:
    expected_output = DataCommandProcessorOutput(events=[], projection_events=[])

    class StubHandler:
        def __init__(self, session):
            self.process = MagicMock(return_value=expected_output)

    processor = DataCommandProcessor(handler=StubHandler, session=MagicMock())
    command = MagicMock()
    user_uid = uuid4()

    output = processor.process(
        command,
        user_uid=user_uid,
        skip_nested_commands=True,
    )

    assert output is expected_output
    processor.handler.process.assert_called_once_with(
        command,
        user_uid=user_uid,
        task_id=None,
        validate_only=False,
        skip_nested_commands=True,
        nested_commands_validate_only=False,
    )


def test_data_command_processor_forwards_nested_commands_validate_only() -> None:
    expected_output = DataCommandProcessorOutput(events=[], projection_events=[])

    class StubHandler:
        def __init__(self, session):
            self.process = MagicMock(return_value=expected_output)

    processor = DataCommandProcessor(handler=StubHandler, session=MagicMock())
    command = MagicMock()
    user_uid = uuid4()

    output = processor.process(
        command,
        user_uid=user_uid,
        nested_commands_validate_only=True,
    )

    assert output is expected_output
    processor.handler.process.assert_called_once_with(
        command,
        user_uid=user_uid,
        task_id=None,
        validate_only=False,
        skip_nested_commands=False,
        nested_commands_validate_only=True,
    )


def test_dryrun_service_propagates_skip_nested_commands(monkeypatch) -> None:
    processor_calls: list[bool] = []

    class StubProcessor:
        def __init__(self, handler, session):
            self.handler = handler
            self.session = session

        def process(self, command, *, user_uid, skip_nested_commands=False, **kwargs):
            processor_calls.append(skip_nested_commands)
            return DataCommandProcessorOutput(events=[], projection_events=[])

    class StubDryrun:
        def __init__(self) -> None:
            self.graph = None

        def model_dump(self, mode="json"):
            return {
                "events": [],
                "skipEvents": None,
                "validationErrors": None,
                "graph": self.graph,
                "timestamp": None,
            }

    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DataCommandProcessor",
        StubProcessor,
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DependencyService.retrieve_item_references_and_dependencies_from_dryrun",
        lambda *args, **kwargs: RefsAndDepsSchema(references=[], dependencies=[]),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.task_dryrun_schema_from_command_output",
        lambda **kwargs: StubDryrun(),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.compile_command_dryrun_graph",
        lambda **kwargs: None,
    )

    session = MagicMock()
    response = CommandDryrunService.dryrun_commands(
        payload=[
            {
                "command_uid": uuid4(),
                "item_id": uuid4(),
                "item_type_id": uuid4(),
                "version": "1.0",
                "command_type": "create",
                "data": {},
                "skip_nested_commands": True,
            }
        ],
        user_uid=uuid4(),
        session=session,
    )

    assert processor_calls == [True]
    assert response.skip_nested_commands is True
    session.rollback.assert_called_once_with()


def test_dryrun_service_propagates_nested_commands_validate_only(monkeypatch) -> None:
    processor_calls: list[bool] = []

    class StubProcessor:
        def __init__(self, handler, session):
            self.handler = handler
            self.session = session

        def process(
            self,
            command,
            *,
            user_uid,
            nested_commands_validate_only=False,
            **kwargs,
        ):
            processor_calls.append(nested_commands_validate_only)
            return DataCommandProcessorOutput(events=[], projection_events=[])

    class StubDryrun:
        def __init__(self) -> None:
            self.graph = None

        def model_dump(self, mode="json"):
            return {
                "events": [],
                "skipEvents": None,
                "validationErrors": None,
                "graph": self.graph,
                "timestamp": None,
            }

    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DataCommandProcessor",
        StubProcessor,
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DependencyService.retrieve_item_references_and_dependencies_from_dryrun",
        lambda *args, **kwargs: RefsAndDepsSchema(references=[], dependencies=[]),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.task_dryrun_schema_from_command_output",
        lambda **kwargs: StubDryrun(),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.compile_command_dryrun_graph",
        lambda **kwargs: None,
    )

    session = MagicMock()
    response = CommandDryrunService.dryrun_commands(
        payload=[
            {
                "command_uid": uuid4(),
                "item_id": uuid4(),
                "item_type_id": uuid4(),
                "version": "1.0",
                "command_type": "create",
                "data": {},
                "nested_commands_validate_only": True,
            }
        ],
        user_uid=uuid4(),
        session=session,
    )

    assert processor_calls == [True]
    assert response.nested_commands_validate_only is True
    session.rollback.assert_called_once_with()


def test_data_command_handler_processes_nested_commands_in_validate_only_mode() -> None:
    handler = DataCommandHandler(session=MagicMock())
    handler.process = MagicMock(
        return_value=DataCommandProcessorOutput(events=[], projection_events=[])
    )
    output = DataCommandProcessorOutput(events=[], projection_events=[])
    user_uid = uuid4()
    nested_command = RefreshItemCommand(
        aggregate_id=uuid4(),
        item_type_id=uuid4(),
        version="default",
        refresh_dependencies=True,
    )

    handler._process_nested_commands(
        commands=[nested_command],
        output=output,
        user_uid=user_uid,
        task_id=None,
        nested_commands_validate_only=True,
    )

    handler.process.assert_called_once_with(
        nested_command,
        user_uid=user_uid,
        task_id=None,
        validate_only=True,
        nested_commands_validate_only=True,
    )


def test_data_command_handler_stops_nested_validation_after_limit() -> None:
    handler = DataCommandHandler(session=MagicMock())
    nested_commands = [
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=uuid4(),
            version="default",
            refresh_dependencies=True,
        )
        for _ in range(NESTED_COMMAND_VALIDATION_ERROR_LIMIT + 2)
    ]
    nested_outputs = [
        DataCommandProcessorOutput(
            events=[],
            projection_events=[],
            validation_errors=[ValidationError(message=f"error-{index}", path=[])],
        )
        for index in range(len(nested_commands))
    ]
    handler.process = MagicMock(side_effect=nested_outputs)
    output = DataCommandProcessorOutput(events=[], projection_events=[])

    handler._process_nested_commands(
        commands=nested_commands,
        output=output,
        user_uid=uuid4(),
        task_id=None,
        nested_commands_validate_only=True,
    )

    assert handler.process.call_count == NESTED_COMMAND_VALIDATION_ERROR_LIMIT
    assert len(output.validation_errors) == NESTED_COMMAND_VALIDATION_ERROR_LIMIT


def test_data_command_handler_round_robins_nested_commands_by_item_type_and_stops_early():
    handler = DataCommandHandler(session=MagicMock())
    user_uid = uuid4()
    item_type_a = uuid4()
    item_type_b = uuid4()
    item_type_c = uuid4()

    nested_commands = [
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=item_type_a,
            version="default",
            refresh_dependencies=True,
        ),
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=item_type_a,
            version="default",
            refresh_dependencies=True,
        ),
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=item_type_b,
            version="default",
            refresh_dependencies=True,
        ),
    ]
    step_two_command = RefreshItemCommand(
        aggregate_id=uuid4(),
        item_type_id=item_type_c,
        version="default",
        refresh_dependencies=True,
    )
    stop_error = ValidationError(message="stop", path=[])
    processed_commands: list[RefreshItemCommand] = []

    def process_side_effect(command, **kwargs):
        processed_commands.append(command)
        nested_output = DataCommandProcessorOutput(events=[], projection_events=[])
        if command is nested_commands[0]:
            handler._process_nested_commands(
                commands=[step_two_command],
                output=nested_output,
                user_uid=user_uid,
                task_id=None,
                nested_commands_validate_only=False,
            )
        elif command is nested_commands[2]:
            nested_output.validation_errors.append(stop_error)
        return nested_output

    handler.process = MagicMock(side_effect=process_side_effect)
    output = DataCommandProcessorOutput(events=[], projection_events=[])

    handler._process_nested_commands(
        commands=nested_commands,
        output=output,
        user_uid=user_uid,
        task_id=None,
        nested_commands_validate_only=False,
    )

    assert processed_commands == [nested_commands[0], nested_commands[2]]
    assert output.validation_errors == [stop_error]


def test_data_command_handler_deduplicates_nested_commands_across_same_step() -> None:
    handler = DataCommandHandler(session=MagicMock())
    user_uid = uuid4()
    item_type_a = uuid4()
    item_type_b = uuid4()
    item_type_c = uuid4()

    nested_commands = [
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=item_type_a,
            version="default",
            refresh_dependencies=True,
        ),
        RefreshItemCommand(
            aggregate_id=uuid4(),
            item_type_id=item_type_b,
            version="default",
            refresh_dependencies=True,
        ),
    ]
    duplicate_step_two_target = RefreshItemCommand(
        aggregate_id=uuid4(),
        item_type_id=item_type_c,
        version="default",
        refresh_dependencies=True,
    )
    processed_commands: list[RefreshItemCommand] = []

    def process_side_effect(command, **kwargs):
        processed_commands.append(command)
        nested_output = DataCommandProcessorOutput(events=[], projection_events=[])
        if command in nested_commands:
            handler._process_nested_commands(
                commands=[RefreshItemCommand.model_validate(duplicate_step_two_target)],
                output=nested_output,
                user_uid=user_uid,
                task_id=None,
                nested_commands_validate_only=False,
            )
        return nested_output

    handler.process = MagicMock(side_effect=process_side_effect)
    output = DataCommandProcessorOutput(events=[], projection_events=[])

    handler._process_nested_commands(
        commands=nested_commands,
        output=output,
        user_uid=user_uid,
        task_id=None,
        nested_commands_validate_only=False,
    )

    assert processed_commands == [
        nested_commands[0],
        nested_commands[1],
        duplicate_step_two_target,
    ]


def test_refresh_item_command_runs_filter_usage_validation_on_refreshed_data(
    monkeypatch,
) -> None:
    handler = DataCommandHandler(session=MagicMock())
    aggregate_id = uuid4()
    item_type_id = uuid4()
    previous_data = {"name": "before"}
    refreshed_data = {"name": "after"}
    aggregate = MagicMock()
    aggregate.next_release = 1
    aggregate.get_item_version.return_value = MagicMock(
        data=previous_data.copy(),
        ref_properties={},
    )

    monkeypatch.setattr(
        data_command_handler_module,
        "_find_aggregate",
        lambda *args, **kwargs: aggregate,
    )
    monkeypatch.setattr(
        data_command_handler_module.SchemaQueryService,
        "parsed_json_schema",
        lambda *args, **kwargs: {"properties": {"name": {"type": "string"}}},
    )
    monkeypatch.setattr(
        handler,
        "_current_item_projection_data",
        lambda **kwargs: previous_data.copy(),
    )
    monkeypatch.setattr(
        data_command_handler_module,
        "_map_refences",
        lambda *args, **kwargs: [],
    )
    monkeypatch.setattr(
        data_command_handler_module,
        "_map_reference_set",
        lambda *args, **kwargs: ({}, []),
    )
    monkeypatch.setattr(
        data_command_handler_module,
        "_handle_reference_set_unique_projection_refresh",
        lambda *args, **kwargs: [
            ItemProjectionRefreshedEvent(
                aggregate_id=aggregate_id,
                data=refreshed_data.copy(),
                ref_properties={},
                item_type_id=item_type_id,
                version="default",
                release=1,
                refresh_dependencies=True,
            )
        ],
    )
    monkeypatch.setattr(
        data_command_handler_module,
        "_handle_mapped_derivations",
        lambda projection_event, json_schema, **kwargs: json_schema,
    )
    monkeypatch.setattr(
        data_command_handler_module,
        "iter_errors",
        lambda *args, **kwargs: [],
    )

    expected_error = ValidationError(message="filter-usage", path=["name"])
    handler._item_filter_usage_validation_errors = MagicMock(
        return_value=[expected_error]
    )

    output = handler.process_refresh_item_command(
        RefreshItemCommand(
            aggregate_id=aggregate_id,
            item_type_id=item_type_id,
            version="default",
            refresh_dependencies=True,
        ),
        user_uid=uuid4(),
    )

    handler._item_filter_usage_validation_errors.assert_called_once_with(
        item_type_id=item_type_id,
        item_id=aggregate_id,
        version="default",
        operation="update",
        modified_fields={"name"},
        current_item_data=previous_data,
    )
    assert output.validation_errors == [expected_error]
    assert output.events == []
    assert output.projection_events == []


def test_dryrun_service_propagates_refresh_dependencies(monkeypatch) -> None:
    refresh_dependency_flags: list[bool] = []

    class StubProcessor:
        def __init__(self, handler, session):
            self.handler = handler
            self.session = session

        def process(self, command, *, user_uid, skip_nested_commands=False, **kwargs):
            refresh_dependency_flags.append(command.refresh_dependencies)
            return DataCommandProcessorOutput(events=[], projection_events=[])

    class StubDryrun:
        def __init__(self) -> None:
            self.graph = None

        def model_dump(self, mode="json"):
            return {
                "events": [],
                "skipEvents": None,
                "validationErrors": None,
                "graph": self.graph,
                "timestamp": None,
            }

    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DataCommandProcessor",
        StubProcessor,
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.DependencyService.retrieve_item_references_and_dependencies_from_dryrun",
        lambda *args, **kwargs: RefsAndDepsSchema(references=[], dependencies=[]),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.task_dryrun_schema_from_command_output",
        lambda **kwargs: StubDryrun(),
    )
    monkeypatch.setattr(
        "dsg_core.services.data.commandDryrunService.TaskDryrunService.compile_command_dryrun_graph",
        lambda **kwargs: None,
    )

    response = CommandDryrunService.dryrun_commands(
        payload=[
            {
                "command_uid": uuid4(),
                "item_id": uuid4(),
                "item_type_id": uuid4(),
                "version": "1.0",
                "command_type": "refresh",
                "data": {},
                "refresh_dependencies": True,
            }
        ],
        user_uid=uuid4(),
        session=MagicMock(),
    )

    assert refresh_dependency_flags == [True]
    assert response.commands[0].error is None
