from pathlib import Path
import time

import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from dais_sdk.types import UserMessage

from src.db.models import tasks as task_models
from src.schemas.tasks import runtime as task_runtime_schemas
from src.schemas.tasks import task as task_schemas
from src.services.exceptions import ServiceErrorCode
from src.services.tasks import TaskNotFoundError, TaskResourceService, TaskService


@pytest.fixture
def task_service(db_session: AsyncSession) -> TaskService:
    return TaskService(db_session)


@pytest.fixture
def task_resource_service(db_session: AsyncSession) -> TaskResourceService:
    return TaskResourceService(db_session, task_runtime_schemas.TaskType.TASK)


@pytest.fixture
def task_resource_data_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    data_dir = tmp_path / "task-data"
    monkeypatch.setattr("src.services.tasks.resource.DATA_DIR", data_dir)
    return data_dir


@pytest.mark.service
@pytest.mark.integration
class TestTaskService:
    @pytest.mark.asyncio
    async def test_get_task_by_id_not_found(self, task_service: TaskService):
        with pytest.raises(TaskNotFoundError, match="Task '999' not found") as exc_info:
            await task_service.get_task_by_id(999)

        assert exc_info.value.error_code == ServiceErrorCode.TASK_NOT_FOUND

    @pytest.mark.asyncio
    async def test_create_task(
        self,
        task_service: TaskService,
        workspace_factory,
        agent_factory,
        tool_factory,
    ):
        tool = await tool_factory(name="Echo", internal_key="echo")
        agent = await agent_factory(name="Agent A", usable_tools=[tool])
        workspace = await workspace_factory(
            name="Workspace A",
            directory="/tmp/workspace-a",
            instruction="Instruction A",
            usable_agents=[agent],
            usable_tools=[tool],
        )

        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )

        assert task.title == "Task A"
        assert task.workspace_id == workspace.id
        assert task.agent_id == agent.id
        assert task.messages == []

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "title,last_run_at,messages",
        [
            ("Task B", 123456, [UserMessage(content="Updated once")]),
            ("Task C", 654321, [UserMessage(content="Updated twice")]),
        ],
        ids=["single-message-update", "alternate-update-values"],
    )
    async def test_update_task_updates_messages_and_fields(
        self,
        task_service: TaskService,
        workspace_factory,
        agent_factory,
        tool_factory,
        title: str,
        last_run_at: int,
        messages: list[UserMessage],
    ):
        tool = await tool_factory(name="Echo", internal_key="echo")
        agent = await agent_factory(name="Agent A", usable_tools=[tool])
        workspace = await workspace_factory(
            name="Workspace A",
            directory="/tmp/workspace-a",
            instruction="Instruction A",
            usable_agents=[agent],
            usable_tools=[tool],
        )
        created = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        updated_usage = task_models.TaskUsage(
            input_tokens=1,
            output_tokens=2,
            total_tokens=3,
            max_tokens=4,
            accumulated_input_tokens=5,
            accumulated_output_tokens=6,
        )

        updated = await task_service.update_task(
            created.id,
            task_schemas.TaskUpdate(
                title=title,
                usage=updated_usage,
                last_run_at=last_run_at,
                agent_id=None,
                messages=messages,
            ),
        )

        assert updated.title == title
        assert updated.last_run_at == last_run_at
        assert updated.agent_id == agent.id
        assert updated.usage == updated_usage
        assert [message.content for message in updated.messages] == [
            message.content for message in messages
        ]

    @pytest.mark.asyncio
    async def test_save_task_resource_creates_db_record_and_file(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        workspace_factory,
        agent_factory,
        task_resource_data_dir: Path,
    ):
        agent = await agent_factory(name="Agent A")
        workspace = await workspace_factory(name="Workspace A")
        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        file_bytes = b"task resource content"

        resource = await task_resource_service.save_task_resource(
            task.id,
            "note.txt",
            file_bytes,
        )
        resource_path = (
            task_resource_data_dir
            / ".task-resources"
            / "task"
            / str(task.id)
            / resource.filename
        )

        assert resource.id is not None
        assert resource.filename.endswith(".txt")
        assert resource.filename != "note.txt"
        assert resource.checksum is not None
        assert resource_path.exists()
        assert resource_path.read_bytes() == file_bytes

    @pytest.mark.asyncio
    async def test_save_task_resource_reuses_existing_record_for_same_checksum(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        workspace_factory,
        agent_factory,
        task_resource_data_dir: Path,
    ):
        agent = await agent_factory(name="Agent A")
        workspace = await workspace_factory(name="Workspace A")
        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        file_bytes = b"same-content"

        first_resource = await task_resource_service.save_task_resource(
            task.id,
            "note.txt",
            file_bytes,
        )
        second_resource = await task_resource_service.save_task_resource(
            task.id,
            "other-name.txt",
            file_bytes,
        )
        resource_dir = (
            task_resource_data_dir / ".task-resources" / "task" / str(task.id)
        )
        stored_files = sorted(path.name for path in resource_dir.iterdir())

        assert second_resource.id == first_resource.id
        assert second_resource.filename == first_resource.filename
        assert stored_files == [first_resource.filename]

    @pytest.mark.asyncio
    async def test_load_task_resource_returns_saved_path(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        workspace_factory,
        agent_factory,
        task_resource_data_dir: Path,
    ):
        agent = await agent_factory(name="Agent A")
        workspace = await workspace_factory(name="Workspace A")
        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        resource = await task_resource_service.save_task_resource(
            task.id,
            "note.txt",
            b"resource-bytes",
        )

        resource_path = await task_resource_service.load_task_resource(task.id, resource.id)

        assert resource_path == (
            task_resource_data_dir
            / ".task-resources"
            / "task"
            / str(task.id)
            / resource.filename
        )

    @pytest.mark.asyncio
    async def test_load_task_resource_returns_none_when_record_or_file_missing(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        workspace_factory,
        agent_factory,
        task_resource_data_dir: Path,
    ):
        agent = await agent_factory(name="Agent A")
        workspace = await workspace_factory(name="Workspace A")
        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        resource = await task_resource_service.save_task_resource(
            task.id,
            "note.txt",
            b"resource-bytes",
        )
        resource_path = (
            task_resource_data_dir
            / ".task-resources"
            / "task"
            / str(task.id)
            / resource.filename
        )
        resource_path.unlink()

        missing_record_result = await task_resource_service.load_task_resource(task.id, 999)
        missing_file_result = await task_resource_service.load_task_resource(
            task.id,
            resource.id,
        )

        assert missing_record_result is None
        assert missing_file_result is None

    @pytest.mark.asyncio
    async def test_cleanup_outdated_tasks_removes_only_older_tasks_and_resources(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        task_resource_data_dir: Path,
        db_session: AsyncSession,
        workspace_factory,
        agent_factory,
    ):
        agent = await agent_factory(name="Agent A")
        workspace = await workspace_factory(name="Workspace A")
        now = int(time.time())

        expired_task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Expired task",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        retained_task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Retained task",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )

        expired_task_in_db = await db_session.get(task_models.Task, expired_task.id)
        retained_task_in_db = await db_session.get(task_models.Task, retained_task.id)
        assert expired_task_in_db is not None
        assert retained_task_in_db is not None

        expired_task_in_db.last_run_at = now - 31 * 24 * 60 * 60
        retained_task_in_db.last_run_at = now - 29 * 24 * 60 * 60
        await db_session.flush()

        expired_resource = await task_resource_service.save_task_resource(
            expired_task.id,
            "expired.txt",
            b"expired-resource",
        )
        retained_resource = await task_resource_service.save_task_resource(
            retained_task.id,
            "retained.txt",
            b"retained-resource",
        )
        expired_resource_dir = (
            task_resource_data_dir / ".task-resources" / "task" / str(expired_task.id)
        )
        retained_resource_dir = (
            task_resource_data_dir / ".task-resources" / "task" / str(retained_task.id)
        )

        assert expired_resource_dir.exists()
        assert retained_resource_dir.exists()

        await task_service.cleanup_outdated_tasks(30)
        await db_session.flush()
        db_session.expunge_all()

        with pytest.raises(TaskNotFoundError, match=f"Task '{expired_task.id}' not found"):
            await task_service.get_task_by_id(expired_task.id)

        retained = await task_service.get_task_by_id(retained_task.id)
        assert retained.id == retained_task.id

        expired_resource_in_db = await db_session.scalar(
            select(task_models.TaskResource).where(
                task_models.TaskResource.id == expired_resource.id
            )
        )
        retained_resource_in_db = await db_session.scalar(
            select(task_models.TaskResource).where(
                task_models.TaskResource.id == retained_resource.id
            )
        )

        assert expired_resource_in_db is None
        assert retained_resource_in_db is not None
        assert not expired_resource_dir.exists()
        assert retained_resource_dir.exists()

    @pytest.mark.asyncio
    async def test_delete_task_removes_entity_and_task_resources(
        self,
        task_service: TaskService,
        task_resource_service: TaskResourceService,
        task_resource_data_dir: Path,
        db_session: AsyncSession,
        workspace_factory,
        agent_factory,
        tool_factory,
    ):
        tool = await tool_factory(name="Echo", internal_key="echo")
        agent = await agent_factory(name="Agent A", usable_tools=[tool])
        workspace = await workspace_factory(
            name="Workspace A",
            directory="/tmp/workspace-a",
            instruction="Instruction A",
            usable_agents=[agent],
            usable_tools=[tool],
        )
        task = await task_service.create_task(
            task_schemas.TaskCreate(
                title="Task A",
                agent_id=agent.id,
                workspace_id=workspace.id,
            )
        )
        resource = await task_resource_service.save_task_resource(
            task.id,
            "note.txt",
            b"resource-bytes",
        )
        resource_dir = task_resource_data_dir / ".task-resources" / "task" / str(task.id)

        assert resource_dir.exists()

        await task_service.delete_task(task.id)
        await db_session.flush()
        db_session.expunge_all()

        with pytest.raises(TaskNotFoundError, match=f"Task '{task.id}' not found"):
            await task_service.get_task_by_id(task.id)

        resource_in_db = await db_session.scalar(
            select(task_models.TaskResource).where(task_models.TaskResource.id == resource.id)
        )
        assert resource_in_db is None
        assert not resource_dir.exists()
