from __future__ import annotations

from typing import Any, Dict, List

import pytest
from pydantic import BaseModel

from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.sync_node import SyncNode
from sync_state_machine.domain.project.api_handler import ProjectApiHandler
from sync_state_machine.domain.project_detail.api_handler import ProjectDetailApiHandler
from sync_state_machine.domain.project_detail.sync_node import ProjectDetailSyncNode
from sync_state_machine.datasource.task_result import TaskStatus


class _ProjectSchema(BaseModel):
    id: str


class _ProjectNode(SyncNode[_ProjectSchema]):
    node_type = "project"
    schema = _ProjectSchema


class _MockApiClient:
    def __init__(self):
        self.requests: List[Dict[str, Any]] = []
        self.push_logs: Dict[str, Dict[str, Any]] = {}
        self.all_info_calls: List[str] = []

    async def request(self, method: str, endpoint: str, params: Dict[str, Any] | None = None, data: Dict[str, Any] | None = None, **kwargs):
        if method == "GET" and endpoint == "/project/all_info":
            project_id = (params or {}).get("project_id")
            if project_id:
                self.all_info_calls.append(str(project_id))
        self.requests.append(
            {
                "method": method,
                "endpoint": endpoint,
                "params": params,
                "data": data,
            }
        )
        return {"code": 200, "data": {"biz_id": (data or {}).get("biz_id")}}

    async def get_push_logs(self, task_ids: List[str]) -> Dict[str, Dict[str, Any]]:
        return {task_id: self.push_logs.get(task_id, {}) for task_id in task_ids}


def _build_project_node(*, node_id: str, project_id: str, all_info: Dict[str, Any]) -> _ProjectNode:
    return _ProjectNode(
        node_id=node_id,
        data_id=project_id,
        data={"id": project_id},
        context={"all_info": all_info},
    )


def _build_project_detail_node(*, node_id: str, data_id: str, project_id: str, key: str, capacity: float = 12.3) -> ProjectDetailSyncNode:
    payload = {
        "id": data_id,
        "project_id": project_id,
        "key": key,
        "value": [{"date": "2026-01-01", "capacity": capacity}],
    }
    return ProjectDetailSyncNode(node_id=node_id, data_id=data_id, data=payload)


def _patch_project_all_info_lookup(
    monkeypatch: pytest.MonkeyPatch,
    *,
    collection: DataCollection,
    call_recorder: List[str] | None = None,
) -> None:
    async def _fake_get_all_info(cls, _client, project_id: str):
        if call_recorder is not None:
            call_recorder.append(project_id)
        node = collection.get_by_data_id("project", project_id)
        assert node is not None
        return node.context.get("all_info", {})

    monkeypatch.setattr(ProjectApiHandler, "get_all_info", classmethod(_fake_get_all_info))


@pytest.mark.asyncio
async def test_create_all_uses_project_update_endpoint_and_returns_in_progress() -> None:
    collection = DataCollection("remote")
    project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()

    node = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
    )

    results = await handler.create_all([node])

    assert len(results) == 1
    assert results[0].status == TaskStatus.IN_PROGRESS
    assert isinstance(results[0].task_id, str) and results[0].task_id

    req = handler.api_client.requests[-1]
    assert req["method"] == "PUT"
    assert req["endpoint"] == "/project/plan_construction"


@pytest.mark.asyncio
async def test_poll_tasks_backfills_real_data_id_from_project_all_info(monkeypatch: pytest.MonkeyPatch) -> None:
    collection = DataCollection("remote")
    project = _build_project_node(
        node_id="p-node",
        project_id="project-1",
        all_info={
            "project_detail": [
                {
                    "id": "remote-detail-1",
                    "project_id": "project-1",
                    "key": "plan_construction",
                    "value": [{"date": "2026-01-01", "capacity": 13.0}],
                }
            ]
        },
    )
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()
    _patch_project_all_info_lookup(monkeypatch, collection=collection)

    node = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
    )

    create_result = (await handler.create_all([node]))[0]
    assert create_result.task_id

    handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}

    polled = await handler.poll_tasks([create_result.task_id])
    final = polled[create_result.task_id]

    assert final.status == TaskStatus.SUCCESS
    assert final.data_id == "remote-detail-1"


@pytest.mark.asyncio
async def test_poll_tasks_fails_when_all_info_missing_detail(monkeypatch: pytest.MonkeyPatch) -> None:
    collection = DataCollection("remote")
    project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()
    _patch_project_all_info_lookup(monkeypatch, collection=collection)

    node = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
    )

    create_result = (await handler.create_all([node]))[0]
    assert create_result.task_id
    handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}

    polled = await handler.poll_tasks([create_result.task_id])
    final = polled[create_result.task_id]

    assert final.status == TaskStatus.FAILED
    assert "project_detail not found in all_info" in (final.error or "")


@pytest.mark.asyncio
async def test_poll_tasks_fails_when_project_detail_has_duplicate_same_key(monkeypatch: pytest.MonkeyPatch) -> None:
    collection = DataCollection("remote")
    project = _build_project_node(
        node_id="p-node",
        project_id="project-1",
        all_info={
            "project_detail": [
                {
                    "id": "remote-detail-1",
                    "project_id": "project-1",
                    "key": "plan_construction",
                    "value": [{"date": "2026-01-01", "capacity": 13.0}],
                },
                {
                    "id": "remote-detail-2",
                    "project_id": "project-1",
                    "key": "plan_construction",
                    "value": [{"date": "2026-01-01", "capacity": 14.0}],
                },
            ]
        },
    )
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()
    _patch_project_all_info_lookup(monkeypatch, collection=collection)

    node = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
    )

    create_result = (await handler.create_all([node]))[0]
    assert create_result.task_id
    handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}

    polled = await handler.poll_tasks([create_result.task_id])
    final = polled[create_result.task_id]

    assert final.status == TaskStatus.FAILED
    assert "duplicated" in (final.error or "")


@pytest.mark.asyncio
async def test_create_all_marks_duplicate_project_key_in_same_batch_as_error() -> None:
    collection = DataCollection("remote")
    project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()

    node1 = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
        capacity=10.0,
    )
    node2 = _build_project_detail_node(
        node_id="detail-node-2",
        data_id="local-detail-2",
        project_id="project-1",
        key="plan_construction",
        capacity=20.0,
    )

    results = await handler.create_all([node1, node2])

    assert results[0].status == TaskStatus.IN_PROGRESS
    assert results[1].status == TaskStatus.FAILED
    assert "Duplicate project_detail_data" in (results[1].error or "")


@pytest.mark.asyncio
async def test_create_node_reuses_existing_node_by_project_key_and_rebinds_data_id() -> None:
    collection = DataCollection("remote")

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()

    existing = _build_project_detail_node(
        node_id="detail-node-old",
        data_id="virtual:project_detail_data:project-1:plan_construction",
        project_id="project-1",
        key="plan_construction",
    )
    await collection.add(existing)

    created = handler._create_node(
        {
            "id": "remote-detail-99",
            "project_id": "project-1",
            "key": "plan_construction",
            "value": [{"date": "2026-01-01", "capacity": 21.0}],
        }
    )

    assert created.node_id == "detail-node-old"
    assert created.data_id == "remote-detail-99"


@pytest.mark.asyncio
async def test_poll_tasks_queries_all_info_once_per_project_for_multiple_keys(monkeypatch: pytest.MonkeyPatch) -> None:
    collection = DataCollection("remote")
    project = _build_project_node(
        node_id="p-node",
        project_id="project-1",
        all_info={"project_detail": []},
    )
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    client = _MockApiClient()
    handler.api_client = client

    node1 = _build_project_detail_node(
        node_id="detail-node-1",
        data_id="local-detail-1",
        project_id="project-1",
        key="plan_construction",
    )
    node2 = _build_project_detail_node(
        node_id="detail-node-2",
        data_id="local-detail-2",
        project_id="project-1",
        key="actual_construction",
    )

    create_results = await handler.create_all([node1, node2])
    task_ids = [r.task_id for r in create_results if r.task_id]
    assert len(task_ids) == 2

    client.push_logs[task_ids[0]] = {"code": 200, "data": {"biz_id": "project-1"}}
    client.push_logs[task_ids[1]] = {"code": 200, "data": {"biz_id": "project-1"}}

    project.context["all_info"] = {
        "project_detail": [
            {
                "id": "remote-detail-1",
                "project_id": "project-1",
                "key": "plan_construction",
                "value": [{"date": "2026-01-01", "capacity": 11.0}],
            },
            {
                "id": "remote-detail-2",
                "project_id": "project-1",
                "key": "actual_construction",
                "value": [{"date": "2026-01-01", "capacity": 22.0}],
            },
        ]
    }
    _patch_project_all_info_lookup(
        monkeypatch,
        collection=collection,
        call_recorder=client.all_info_calls,
    )

    polled = await handler.poll_tasks(task_ids)

    assert polled[task_ids[0]].status == TaskStatus.SUCCESS
    assert polled[task_ids[0]].data_id == "remote-detail-1"
    assert polled[task_ids[1]].status == TaskStatus.SUCCESS
    assert polled[task_ids[1]].data_id == "remote-detail-2"
    assert client.all_info_calls == ["project-1"]


@pytest.mark.asyncio
async def test_poll_tasks_retries_until_project_detail_appears(monkeypatch: pytest.MonkeyPatch) -> None:
    collection = DataCollection("remote")
    project = _build_project_node(node_id="p-node", project_id="project-1", all_info={"project_detail": []})
    await collection.add(project)

    handler = ProjectDetailApiHandler()
    handler.set_collection(collection)
    handler.api_client = _MockApiClient()

    call_count = {"value": 0}

    async def _fake_get_all_info(cls, _client, project_id: str):
        call_count["value"] += 1
        if call_count["value"] >= 2:
            return {
                "project_detail": [
                    {
                        "id": "remote-detail-1",
                        "project_id": project_id,
                        "key": "ensure_production",
                        "value": [{"year": 2026, "capacity": 21.0}],
                    }
                ]
            }
        return {"project_detail": []}

    monkeypatch.setattr(ProjectApiHandler, "get_all_info", classmethod(_fake_get_all_info))

    node = ProjectDetailSyncNode(
        node_id="detail-node-1",
        data_id="local-detail-1",
        data={
            "id": "local-detail-1",
            "project_id": "project-1",
            "key": "ensure_production",
            "value": [{"year": 2026, "capacity": 21.0}],
        },
    )

    create_result = (await handler.create_all([node]))[0]
    assert create_result.task_id
    handler.api_client.push_logs[create_result.task_id] = {"code": 200, "data": {"biz_id": "project-1"}}

    polled = await handler.poll_tasks([create_result.task_id])
    final = polled[create_result.task_id]

    assert final.status == TaskStatus.SUCCESS
    assert final.data_id == "remote-detail-1"
    assert call_count["value"] >= 2
