"""Tests for instance sync cursor validation handler and cursor state helpers."""

from __future__ import annotations

from unittest.mock import MagicMock, Mock, patch


class TestHandleInstanceSyncCursorValidation:
    """Tests for handle_instance_sync_cursor_validation."""

    def _make_data(self, **overrides):
        base = {
            "view_space": "sp_test",
            "view_external_id": "TestView",
            "view_version": "v1",
            "instance_space": "inst_space",
            "shacl_rules_file_external_id": "test_rules",
            "datamodel_space": "sp_dm",
            "datamodel_external_id": "TestDM",
            "datamodel_version": "v1",
            "records_config": {
                "stream_id": "stream1",
                "rule_set_id": "TestRuleSet",
                "rule_set_version": "1.0",
            },
        }
        base.update(overrides)
        return base

    def test_returns_error_when_shacl_load_fails(self):
        """Handler returns error dict when SHACL rules cannot be loaded."""
        from cognite_data_quality._function_code.handlers.instance_sync_cursor_validation import (
            handle_instance_sync_cursor_validation,
        )

        mock_client = Mock()
        data = self._make_data()

        with patch(
            "cognite_data_quality._function_code.handlers.instance_sync_cursor_validation.load_shacl_from_file",
            side_effect=Exception("file not found"),
        ):
            result = handle_instance_sync_cursor_validation(data, mock_client)

        assert result["status"] == "error"
        assert "Failed to load SHACL rules" in result["error"]

    def test_applies_initial_sync_cutoff_filter_on_first_run(self):
        """On first run (no cursor), lastUpdatedTime filter is added when initial_sync_cutoff set."""
        from cognite_data_quality._function_code.handlers.instance_sync_cursor_validation import (
            handle_instance_sync_cursor_validation,
        )

        mock_client = Mock()
        sync_result = MagicMock()
        sync_result.data = {"nodes": []}
        sync_result.cursors = MagicMock()
        sync_result.cursors.__dict__ = {"nodes": None}

        captured_queries = []

        def capture_sync(**kwargs):
            captured_queries.append(kwargs.get("query"))
            return sync_result

        mock_client.data_modeling.instances.sync.side_effect = capture_sync

        data = self._make_data(initial_sync_cutoff="2024-01-01T00:00:00Z")

        with (
            patch(
                "cognite_data_quality._function_code.handlers.instance_sync_cursor_validation.load_shacl_from_file",
                return_value="@prefix sh: <http://www.w3.org/ns/shacl#> .",
            ),
            patch(
                "cognite_data_quality._function_code.handlers.instance_sync_cursor_validation.load_sync_cursor",
                return_value=None,
            ),
            patch("cognite_data_quality._neat.NeatSession"),
        ):
            result = handle_instance_sync_cursor_validation(data, mock_client)

        assert result["status"] == "completed"
        assert captured_queries, "sync should have been called"
        query = captured_queries[0]
        node_expr = query.with_["nodes"]
        # With initial_sync_cutoff: And has 3 sub-filters (HasData, Equals, Range on lastUpdatedTime)
        dumped = node_expr.filter.dump()
        assert len(dumped["and"]) == 3
        assert any("range" in f for f in dumped["and"])

    def test_no_cutoff_filter_when_cursor_exists(self):
        """When a cursor already exists, the lastUpdatedTime filter is NOT added."""
        from cognite_data_quality._function_code.handlers.instance_sync_cursor_validation import (
            handle_instance_sync_cursor_validation,
        )

        mock_client = Mock()
        sync_result = MagicMock()
        sync_result.data = {"nodes": []}
        sync_result.cursors = MagicMock()
        sync_result.cursors.__dict__ = {"nodes": None}

        captured_queries = []

        def capture_sync(**kwargs):
            captured_queries.append(kwargs.get("query"))
            return sync_result

        mock_client.data_modeling.instances.sync.side_effect = capture_sync

        data = self._make_data(initial_sync_cutoff="2024-01-01T00:00:00Z")

        with (
            patch(
                "cognite_data_quality._function_code.handlers.instance_sync_cursor_validation.load_shacl_from_file",
                return_value="@prefix sh: <http://www.w3.org/ns/shacl#> .",
            ),
            patch(
                "cognite_data_quality._function_code.handlers.instance_sync_cursor_validation.load_sync_cursor",
                return_value={"cursor": "existing_cursor_abc", "processed_count": 500},
            ),
            patch("cognite_data_quality._neat.NeatSession"),
        ):
            handle_instance_sync_cursor_validation(data, mock_client)

        assert captured_queries
        query = captured_queries[0]
        node_expr = query.with_["nodes"]
        # With existing cursor: only And(HasData, Equals(space)) = 2 sub-filters (no Range)
        dumped = node_expr.filter.dump()
        assert len(dumped["and"]) == 2
        assert not any("range" in f for f in dumped["and"])


class TestSyncCursorState:
    """Tests for load_sync_cursor and save_sync_cursor."""

    def test_load_sync_cursor_returns_none_when_not_found(self):
        """load_sync_cursor returns None when no state exists."""
        from cognite_data_quality._function_code.common.cursor_state import load_sync_cursor

        mock_client = Mock()
        mock_response = Mock()
        mock_response.json.return_value = {"items": []}
        mock_client.post.return_value = mock_response

        result = load_sync_cursor(mock_client, "sp", "MyView", "inst_space", "orch_123")

        assert result is None

    def test_load_sync_cursor_returns_cursor_when_found(self):
        """load_sync_cursor extracts cursor and processed_count from DMS response."""
        from cognite_data_quality._function_code.common.cursor_state import load_sync_cursor

        mock_client = Mock()
        mock_response = Mock()
        mock_response.json.return_value = {
            "items": [
                {
                    "externalId": "sync_cursor_orch_123_sp_MyView_inst_space",
                    "properties": {
                        "dataQuality": {
                            "FunctionValidationStateView/1": {
                                "syncCursor": "cursor_xyz_123",
                                "processedCount": 42,
                            }
                        }
                    },
                }
            ]
        }
        mock_client.post.return_value = mock_response

        result = load_sync_cursor(mock_client, "sp", "MyView", "inst_space", "orch_123")

        assert result is not None
        assert result["cursor"] == "cursor_xyz_123"
        assert result["processed_count"] == 42

    def test_load_sync_cursor_returns_none_on_exception(self):
        """load_sync_cursor returns None on API error (non-fatal)."""
        from cognite_data_quality._function_code.common.cursor_state import load_sync_cursor

        mock_client = Mock()
        mock_client.post.side_effect = Exception("network error")

        result = load_sync_cursor(mock_client, "sp", "MyView", "inst_space", "orch_123")

        assert result is None

    def test_save_sync_cursor_upserts_node(self):
        """save_sync_cursor calls instances.apply with correct external_id."""
        from cognite_data_quality._function_code.common.cursor_state import save_sync_cursor

        mock_client = Mock()
        mock_client.data_modeling.instances.apply.return_value = Mock()

        save_sync_cursor(mock_client, "sp", "MyView", "inst_space", "cursor_abc", 100, "orch_123")

        mock_client.data_modeling.instances.apply.assert_called_once()
        call_kwargs = mock_client.data_modeling.instances.apply.call_args
        nodes = call_kwargs.kwargs.get("nodes") or call_kwargs.args[0]
        assert nodes[0].external_id == "sync_cursor_orch_123_sp_MyView_inst_space"

    def test_save_sync_cursor_raises_on_api_error(self):
        """save_sync_cursor raises RuntimeError when DMS write fails."""
        import pytest
        from cognite_data_quality._function_code.common.cursor_state import save_sync_cursor

        mock_client = Mock()
        mock_client.data_modeling.instances.apply.side_effect = Exception("write failed")

        with pytest.raises(RuntimeError, match="CRITICAL ERROR saving sync cursor"):
            save_sync_cursor(mock_client, "sp", "MyView", "inst_space", "cursor_abc", 100, "orch_123")
