# #region test_task_persistence [C:2] [TYPE Module]
# @RELATION  BINDS_TO -> SrcRoot
# @SEMANTICS: test, task, persistence, unit_test
# @PURPOSE: Unit tests for TaskPersistenceService.
# @LAYER Tests
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}

from datetime import UTC, datetime
from unittest.mock import patch

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from src.core.task_manager.models import LogEntry, Task, TaskStatus
from src.core.task_manager.persistence import TaskPersistenceService
from src.models.mapping import Base, Environment
from src.models.task import TaskRecord


# #region TestTaskPersistenceHelpers [C:2] [TYPE Class]
# @RELATION  BINDS_TO -> test_task_persistence
# @PURPOSE: Test suite for TaskPersistenceService static helper methods.
class TestTaskPersistenceHelpers:

    # #region test_json_load_if_needed_none [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with None input.
    def test_json_load_if_needed_none(self):
        assert TaskPersistenceService._json_load_if_needed(None) is None
    # #endregion test_json_load_if_needed_none

    # #region test_json_load_if_needed_dict [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with dict input.
    def test_json_load_if_needed_dict(self):
        data = {"key": "value"}
        assert TaskPersistenceService._json_load_if_needed(data) == data
    # #endregion test_json_load_if_needed_dict

    # #region test_json_load_if_needed_list [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with list input.
    def test_json_load_if_needed_list(self):
        data = [1, 2, 3]
        assert TaskPersistenceService._json_load_if_needed(data) == data
    # #endregion test_json_load_if_needed_list

    # #region test_json_load_if_needed_json_string [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with JSON string.
    def test_json_load_if_needed_json_string(self):
        result = TaskPersistenceService._json_load_if_needed('{"key": "value"}')
        assert result == {"key": "value"}
    # #endregion test_json_load_if_needed_json_string

    # #region test_json_load_if_needed_empty_string [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with empty/null strings.
    def test_json_load_if_needed_empty_string(self):
        assert TaskPersistenceService._json_load_if_needed("") is None
        assert TaskPersistenceService._json_load_if_needed("null") is None
        assert TaskPersistenceService._json_load_if_needed("  null  ") is None
    # #endregion test_json_load_if_needed_empty_string

    # #region test_json_load_if_needed_plain_string [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with non-JSON string.
    def test_json_load_if_needed_plain_string(self):
        result = TaskPersistenceService._json_load_if_needed("not json")
        assert result == "not json"
    # #endregion test_json_load_if_needed_plain_string

    # #region test_json_load_if_needed_integer [C:2] [TYPE Function]
    # @PURPOSE: Test _json_load_if_needed with integer.
    def test_json_load_if_needed_integer(self):
        assert TaskPersistenceService._json_load_if_needed(42) == 42
    # #endregion test_json_load_if_needed_integer

    # #region test_parse_datetime_none [C:2] [TYPE Function]
    # @PURPOSE: Test _parse_datetime with None.
    def test_parse_datetime_none(self):
        assert TaskPersistenceService._parse_datetime(None) is None
    # #endregion test_parse_datetime_none

    # #region test_parse_datetime_datetime_object [C:2] [TYPE Function]
    # @PURPOSE: Test _parse_datetime with datetime object.
    def test_parse_datetime_datetime_object(self):
        dt = datetime(2024, 1, 1, 12, 0, 0)
        assert TaskPersistenceService._parse_datetime(dt) == dt
    # #endregion test_parse_datetime_datetime_object

    # #region test_parse_datetime_iso_string [C:2] [TYPE Function]
    # @PURPOSE: Test _parse_datetime with ISO string.
    def test_parse_datetime_iso_string(self):
        result = TaskPersistenceService._parse_datetime("2024-01-01T12:00:00")
        assert isinstance(result, datetime)
        assert result.year == 2024
    # #endregion test_parse_datetime_iso_string

    # #region test_parse_datetime_invalid_string [C:2] [TYPE Function]
    # @PURPOSE: Test _parse_datetime with invalid string.
    def test_parse_datetime_invalid_string(self):
        assert TaskPersistenceService._parse_datetime("not-a-date") is None
    # #endregion test_parse_datetime_invalid_string

    # #region test_parse_datetime_integer [C:2] [TYPE Function]
    # @PURPOSE: Test _parse_datetime with non-string, non-datetime.
    def test_parse_datetime_integer(self):
        assert TaskPersistenceService._parse_datetime(12345) is None
    # #endregion test_parse_datetime_integer

# #endregion TestTaskPersistenceHelpers


# #region TestTaskPersistenceService [C:2] [TYPE Class]
# @RELATION  BINDS_TO -> test_task_persistence
# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
class TestTaskPersistenceService:

    # #region setup_class [C:2] [TYPE Function]
    # @PURPOSE: Setup in-memory test database.
    @classmethod
    def setup_class(cls):
        """Create an in-memory SQLite database for testing."""
        cls.engine = create_engine("sqlite:///:memory:")
        from sqlalchemy import event
        event.listen(cls.engine, "connect", lambda c, _: c.execute("PRAGMA foreign_keys=ON"))
        Base.metadata.create_all(bind=cls.engine)
        cls.TestSessionLocal = sessionmaker(bind=cls.engine)
        cls.service = TaskPersistenceService()
    # #endregion setup_class

    # #region teardown_class [C:2] [TYPE Function]
    # @PURPOSE: Dispose of test database.
    @classmethod
    def teardown_class(cls):
        cls.engine.dispose()
    # #endregion teardown_class

    # #region setup_method [C:2] [TYPE Function]
    # @PURPOSE: Clean task_records table before each test.
    def setup_method(self):
        session = self.TestSessionLocal()
        session.query(TaskRecord).delete()
        session.query(Environment).delete()
        session.commit()
        session.close()
    # #endregion setup_method

    def _patched(self):
        """Helper: returns a patch context for TasksSessionLocal."""
        return patch(
            "src.core.task_manager.persistence.TasksSessionLocal",
            self.TestSessionLocal
        )

    def _make_task(self, **kwargs):
        """Helper: create a Task with test defaults."""
        defaults = {
            "id": "test-uuid-1",
            "plugin_id": "backup",
            "status": TaskStatus.PENDING,
            "params": {"source_env_id": "env-1"},
        }
        defaults.update(kwargs)
        return Task(**defaults)

    # #region test_persist_task_new [C:2] [TYPE Function]
    # @PURPOSE: Test persisting a new task creates a record.
    # @PRE: Empty database.
    # @POST: TaskRecord exists in database.
    def test_persist_task_new(self):
        """Test persisting a new task creates a record."""
        task = self._make_task()

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record is not None
        assert record.type == "backup"
        assert record.status == "PENDING"
    # #endregion test_persist_task_new

    # #region test_persist_task_update [C:2] [TYPE Function]
    # @PURPOSE: Test updating an existing task.
    # @PRE: Task already persisted.
    # @POST: Task record updated with new status.
    def test_persist_task_update(self):
        """Test persisting an existing task updates the record."""
        task = self._make_task(status=TaskStatus.PENDING)

        with self._patched():
            self.service.persist_task(task)

        # Update status
        task.status = TaskStatus.RUNNING
        task.started_at = datetime.now(UTC)

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record.status == "RUNNING"
        assert record.started_at is not None
    # #endregion test_persist_task_update

    # #region test_persist_task_with_logs [C:2] [TYPE Function]
    # @PURPOSE: Test persisting a task with log entries.
    # @PRE: Task has logs attached.
    # @POST: Logs serialized as JSON in task record.
    def test_persist_task_with_logs(self):
        """Test persisting a task with log entries."""
        task = self._make_task()
        task.logs = [
            LogEntry(message="Step 1", level="INFO", source="plugin"),
            LogEntry(message="Step 2", level="INFO", source="plugin"),
        ]

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record.logs is not None
        assert len(record.logs) == 2
    # #endregion test_persist_task_with_logs

    # #region test_persist_task_failed_extracts_error [C:2] [TYPE Function]
    # @PURPOSE: Test that FAILED task extracts last error message.
    # @PRE: Task has FAILED status with ERROR logs.
    # @POST: record.error contains last error message.
    def test_persist_task_failed_extracts_error(self):
        """Test that FAILED tasks extract the last error message."""
        task = self._make_task(status=TaskStatus.FAILED)
        task.logs = [
            LogEntry(message="Started OK", level="INFO", source="plugin"),
            LogEntry(message="Connection failed", level="ERROR", source="plugin"),
            LogEntry(message="Retrying...", level="INFO", source="plugin"),
            LogEntry(message="Fatal: timeout", level="ERROR", source="plugin"),
        ]

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record.error == "Fatal: timeout"
    # #endregion test_persist_task_failed_extracts_error

    # #region test_persist_tasks_batch [C:2] [TYPE Function]
    # @PURPOSE: Test persisting multiple tasks.
    # @PRE: Empty database.
    # @POST: All task records created.
    def test_persist_tasks_batch(self):
        """Test persisting multiple tasks at once."""
        tasks = [
            self._make_task(id=f"batch-{i}", plugin_id="migration")
            for i in range(3)
        ]

        with self._patched():
            self.service.persist_tasks(tasks)

        session = self.TestSessionLocal()
        count = session.query(TaskRecord).count()
        session.close()

        assert count == 3
    # #endregion test_persist_tasks_batch

    # #region test_load_tasks [C:2] [TYPE Function]
    # @PURPOSE: Test loading tasks from database.
    # @PRE: Tasks persisted.
    # @POST: Returns list of Task objects with correct data.
    def test_load_tasks(self):
        """Test loading tasks from database (round-trip)."""
        task = self._make_task(
            status=TaskStatus.SUCCESS,
            started_at=datetime.now(UTC),
            finished_at=datetime.now(UTC),
        )
        task.params = {"key": "value"}
        task.result = {"output": "done"}
        task.logs = [LogEntry(message="Done", level="INFO", source="plugin")]

        with self._patched():
            self.service.persist_task(task)

        with self._patched():
            loaded = self.service.load_tasks(limit=10)

        assert len(loaded) == 1
        assert loaded[0].id == "test-uuid-1"
        assert loaded[0].plugin_id == "backup"
        assert loaded[0].status == TaskStatus.SUCCESS
        assert loaded[0].params == {"key": "value"}
    # #endregion test_load_tasks

    # #region test_load_tasks_with_status_filter [C:2] [TYPE Function]
    # @PURPOSE: Test loading tasks filtered by status.
    # @PRE: Tasks with different statuses persisted.
    # @POST: Returns only tasks matching status filter.
    def test_load_tasks_with_status_filter(self):
        """Test loading tasks filtered by status."""
        tasks = [
            self._make_task(id="s1", status=TaskStatus.SUCCESS),
            self._make_task(id="s2", status=TaskStatus.FAILED),
            self._make_task(id="s3", status=TaskStatus.SUCCESS),
        ]

        with self._patched():
            self.service.persist_tasks(tasks)

        with self._patched():
            failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)

        assert len(failed_tasks) == 1
        assert failed_tasks[0].id == "s2"
        assert failed_tasks[0].status == TaskStatus.FAILED
    # #endregion test_load_tasks_with_status_filter

    # #region test_load_tasks_with_limit [C:2] [TYPE Function]
    # @PURPOSE: Test loading tasks with limit.
    # @PRE: Multiple tasks persisted.
    # @POST: Returns at most `limit` tasks.
    def test_load_tasks_with_limit(self):
        """Test loading tasks respects limit parameter."""
        tasks = [
            self._make_task(id=f"lim-{i}")
            for i in range(10)
        ]

        with self._patched():
            self.service.persist_tasks(tasks)

        with self._patched():
            loaded = self.service.load_tasks(limit=3)

        assert len(loaded) == 3
    # #endregion test_load_tasks_with_limit

    # #region test_delete_tasks [C:2] [TYPE Function]
    # @PURPOSE: Test deleting tasks by ID list.
    # @PRE: Tasks persisted.
    # @POST: Specified tasks deleted, others remain.
    def test_delete_tasks(self):
        """Test deleting tasks by ID list."""
        tasks = [
            self._make_task(id="del-1"),
            self._make_task(id="del-2"),
            self._make_task(id="keep-1"),
        ]

        with self._patched():
            self.service.persist_tasks(tasks)

        with self._patched():
            self.service.delete_tasks(["del-1", "del-2"])

        session = self.TestSessionLocal()
        remaining = session.query(TaskRecord).all()
        session.close()

        assert len(remaining) == 1
        assert remaining[0].id == "keep-1"
    # #endregion test_delete_tasks

    # #region test_delete_tasks_empty_list [C:2] [TYPE Function]
    # @PURPOSE: Test deleting with empty list (no-op).
    # @PRE: None.
    # @POST: No error, no changes.
    def test_delete_tasks_empty_list(self):
        """Test deleting with empty list is a no-op."""
        with self._patched():
            self.service.delete_tasks([])  # Should not raise
    # #endregion test_delete_tasks_empty_list

    # #region test_persist_task_with_datetime_in_params [C:2] [TYPE Function]
    # @PURPOSE: Test json_serializable handles datetime in params.
    # @PRE: Task params contain datetime values.
    # @POST: Params serialized correctly.
    def test_persist_task_with_datetime_in_params(self):
        """Test that datetime values in params are serialized to ISO format."""
        dt = datetime(2024, 6, 15, 10, 30, 0)
        task = self._make_task(params={"timestamp": dt, "name": "test"})

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record.params is not None
        assert record.params["timestamp"] == "2024-06-15T10:30:00"
        assert record.params["name"] == "test"
    # #endregion test_persist_task_with_datetime_in_params

    # #region test_persist_task_resolves_environment_slug_to_existing_id [C:2] [TYPE Function]
    # @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.
    # @PRE: environments table contains env with name convertible to provided slug token.
    # @POST: task_records.environment_id stores actual environments.id and does not violate FK.
    def test_persist_task_resolves_environment_slug_to_existing_id(self):
        session = self.TestSessionLocal()
        env = Environment(id="env-uuid-1", name="SS DEV", url="https://example.local", credentials_id="cred-1")
        session.add(env)
        session.commit()
        session.close()

        task = self._make_task(params={"environment_id": "ss-dev"})

        with self._patched():
            self.service.persist_task(task)

        session = self.TestSessionLocal()
        record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
        session.close()

        assert record is not None
        assert record.environment_id == "env-uuid-1"
    # #endregion test_persist_task_resolves_environment_slug_to_existing_id

# #endregion TestTaskPersistenceService
# #endregion test_task_persistence
