from __future__ import annotations

from unittest.mock import AsyncMock, patch

import pytest

from src.bulk_operations import BulkOperationConfig, BulkOperationManager
from src.meta_client import MetaClient


@pytest.mark.asyncio
async def test_fetch_campaigns_binds_chunk_values_and_cache_keys() -> None:
    client = AsyncMock(spec=MetaClient)

    async def fake_get(path: str, params=None):
        if params and params.get("offset") is not None:
            return {"data": [{"offset": params["offset"], "limit": params["limit"]}]}
        return {"data": [{"id": "seed"}]}

    client.get.side_effect = fake_get
    manager = BulkOperationManager(
        client=client,
        config=BulkOperationConfig(chunk_size=2, max_concurrent=10, enable_caching=True),
    )
    observed_cache_keys: list[str] = []

    async def fake_cached_fetch(cache_key: str, operation, ttl=None):
        observed_cache_keys.append(cache_key)
        return await operation()

    with patch.object(manager, "_fetch_cached_or_execute", new=AsyncMock(side_effect=fake_cached_fetch)):
        results = await manager.fetch_campaigns("act_123", fields=["id"], limit=5)

    assert len(results) == 3
    assert len(observed_cache_keys) == 3
    assert len(set(observed_cache_keys)) == 3

    offsets = [result.data["data"][0]["offset"] for result in results if result.success and result.data]
    assert sorted(offsets) == [0, 2, 4]


@pytest.mark.asyncio
async def test_fetch_insights_binds_object_chunks_and_cache_keys() -> None:
    client = AsyncMock(spec=MetaClient)

    async def fake_get(path: str, params=None):
        filtering = params.get("filtering", []) if params else []
        object_ids = filtering[0]["value"] if filtering else []
        return {"data": [{"object_ids": object_ids}]}

    client.get.side_effect = fake_get
    manager = BulkOperationManager(
        client=client,
        config=BulkOperationConfig(chunk_size=2, max_concurrent=10, enable_caching=True),
    )
    observed_cache_keys: list[str] = []

    async def fake_cached_fetch(cache_key: str, operation, ttl=None):
        observed_cache_keys.append(cache_key)
        return await operation()

    with patch.object(manager, "_fetch_cached_or_execute", new=AsyncMock(side_effect=fake_cached_fetch)):
        results = await manager.fetch_insights(
            ad_account_id="act_123",
            object_ids=["o1", "o2", "o3", "o4", "o5"],
            fields=["impressions"],
            level="ad",
        )

    assert len(results) == 3
    assert len(observed_cache_keys) == 3
    assert len(set(observed_cache_keys)) == 3

    seen_chunks = sorted(
        tuple(result.data["data"][0]["object_ids"])
        for result in results
        if result.success and result.data
    )
    assert seen_chunks == [("o1", "o2"), ("o3", "o4"), ("o5",)]


@pytest.mark.asyncio
async def test_bulk_update_binds_batch_payloads_per_task() -> None:
    client = AsyncMock(spec=MetaClient)
    client.batch.side_effect = lambda operations: [{"first": operations[0]["relative_url"], "size": len(operations)}]

    manager = BulkOperationManager(
        client=client,
        config=BulkOperationConfig(max_concurrent=10, enable_caching=False),
    )

    updates = [{"id": str(i), "status": "ACTIVE"} for i in range(51)]
    results = await manager.bulk_update(updates)

    assert len(results) == 2
    assert client.batch.await_count == 2
    first_batch = client.batch.await_args_list[0].args[0]
    second_batch = client.batch.await_args_list[1].args[0]
    assert first_batch[0]["relative_url"] == "0"
    assert second_batch[0]["relative_url"] == "50"
    assert len(first_batch) == 50
    assert len(second_batch) == 1


@pytest.mark.asyncio
async def test_parallel_insights_by_campaign_binds_campaign_and_cache_key() -> None:
    client = AsyncMock(spec=MetaClient)

    async def fake_get(path: str, params=None):
        campaign_id = path.split("/", maxsplit=1)[0]
        return {"data": [{"campaign_id": campaign_id}]}

    client.get.side_effect = fake_get
    manager = BulkOperationManager(
        client=client,
        config=BulkOperationConfig(max_concurrent=10, enable_caching=True),
    )
    observed_cache_keys: list[str] = []

    async def fake_cached_fetch(cache_key: str, operation, ttl=None):
        observed_cache_keys.append(cache_key)
        return await operation()

    with patch.object(manager, "_fetch_cached_or_execute", new=AsyncMock(side_effect=fake_cached_fetch)):
        results = await manager.parallel_insights_by_campaign(
            ad_account_id="act_123",
            campaign_ids=["c1", "c2", "c3"],
            fields=["impressions"],
        )

    assert len(observed_cache_keys) == 3
    assert len(set(observed_cache_keys)) == 3
    assert results["c1"]["insights"]["campaign_id"] == "c1"
    assert results["c2"]["insights"]["campaign_id"] == "c2"
    assert results["c3"]["insights"]["campaign_id"] == "c3"
