"""Aggregation of per-target dispatch results into ``EventLog.details``.

Covers ``summarize_dispatch_results`` and ``attach_summary_in_place``.
The async ``record_dispatch_summary_async`` is exercised through the
in-process update path; the watcher-style flow is covered indirectly via
the full server tests.
"""

from __future__ import annotations

from typing import Any

import pytest


def test_summarize_empty_returns_empty(tmp_data_dir) -> None:  # noqa: ARG001
    """Empty results = nothing to summarize. Callers can short-circuit
    on the falsy return so a row with zero dispatches doesn't get a
    misleading zero-counts block."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    assert summarize_dispatch_results([]) == {}


def test_summarize_all_success_no_errors_block(tmp_data_dir) -> None:  # noqa: ARG001
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {"success": True, "message_id": 1},
        {"success": True, "message_id": 2},
    ]
    summary = summarize_dispatch_results(results)
    assert summary["targets_attempted"] == 2
    assert summary["targets_succeeded"] == 2
    assert summary["targets_failed"] == 0
    assert "errors" not in summary
    assert "media" not in summary


def test_summarize_mixed_records_only_failures(tmp_data_dir) -> None:  # noqa: ARG001
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {"success": True},
        {"success": False, "error": "Bad Request: chat not found"},
        {"success": False, "error": "timeout"},
    ]
    summary = summarize_dispatch_results(results)
    assert summary["targets_succeeded"] == 1
    assert summary["targets_failed"] == 2
    assert summary["errors"] == [
        {"index": 1, "error": "Bad Request: chat not found"},
        {"index": 2, "error": "timeout"},
    ]


def test_summarize_media_counts_aggregate(tmp_data_dir) -> None:  # noqa: ARG001
    """Media counts from a Telegram media-group success are merged."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {
            "success": True,
            "delivered_count": 5,
            "skipped_count": 1,
            "failed_count": 0,
        },
        {
            "success": True,
            "delivered_count": 3,
            "skipped_count": 0,
            "failed_count": 0,
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["media"] == {"delivered": 8, "skipped": 1, "failed": 0}


def test_summarize_sub_errors_carry_target_index(tmp_data_dir) -> None:  # noqa: ARG001
    """Per-chunk/per-item failures from a partial media-group send are flattened."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {"success": True, "delivered_count": 1, "skipped_count": 0, "failed_count": 0},
        {
            "success": True,  # group landed but with partial failure
            "delivered_count": 2,
            "skipped_count": 0,
            "failed_count": 1,
            "errors": [
                {"kind": "chunk", "chunk": 1, "error": "Bad Request: ..."},
                {"kind": "item", "chunk": 1, "item_index": 2, "error": "media not found"},
            ],
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["media_errors"] == [
        {"target_index": 1, "kind": "chunk", "chunk": 1, "error": "Bad Request: ..."},
        {
            "target_index": 1,
            "kind": "item",
            "chunk": 1,
            "item_index": 2,
            "error": "media not found",
        },
    ]


def test_summarize_caps_errors_and_reports_truncation(tmp_data_dir) -> None:  # noqa: ARG001
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results: list[dict[str, Any]] = [
        {"success": False, "error": f"err {i}"} for i in range(25)
    ]
    summary = summarize_dispatch_results(results)
    assert len(summary["errors"]) == 20
    assert summary["errors_truncated"] == 5


def test_summarize_trims_long_error_messages(tmp_data_dir) -> None:  # noqa: ARG001
    """A pathological multi-KB error string is bounded so the row stays small."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    long_err = "x" * 2000
    results = [{"success": False, "error": long_err}]
    summary = summarize_dispatch_results(results)
    persisted = summary["errors"][0]["error"]
    assert persisted.endswith("…[truncated]")
    # 500 char body + the explicit "…[truncated]" marker.
    assert len(persisted) == 500 + len("…[truncated]")


@pytest.mark.asyncio
async def test_attach_summary_in_place_mutates_details_dict(tmp_data_dir) -> None:  # noqa: ARG001
    """In-session call merges the summary without losing original keys."""
    from notify_bridge_server.database.models import EventLog
    from notify_bridge_server.services.dispatch_summary import (
        attach_summary_in_place,
    )

    row = EventLog(
        event_type="assets_added",
        collection_id="abc",
        collection_name="Album",
        details={"provider_type": "immich", "added_count": 3},
    )
    attach_summary_in_place(row, [{"success": True}, {"success": False, "error": "x"}])
    assert row.details["provider_type"] == "immich"
    assert row.details["added_count"] == 3
    assert row.details["dispatch_summary"] == {
        "targets_attempted": 2,
        "targets_succeeded": 1,
        "targets_failed": 1,
        "errors": [{"index": 1, "error": "x"}],
    }


@pytest.mark.asyncio
async def test_attach_summary_in_place_with_no_results_is_noop(tmp_data_dir) -> None:  # noqa: ARG001
    """Empty results → no ``dispatch_summary`` key written. Original
    details survive untouched."""
    from notify_bridge_server.database.models import EventLog
    from notify_bridge_server.services.dispatch_summary import (
        attach_summary_in_place,
    )

    row = EventLog(
        event_type="assets_added",
        collection_id="abc",
        collection_name="Album",
        details={"k": "v"},
    )
    attach_summary_in_place(row, [])
    assert row.details == {"k": "v"}
    assert "dispatch_summary" not in row.details


def test_summarize_handles_malformed_sub_errors(tmp_data_dir) -> None:  # noqa: ARG001
    """A non-dict sub-error entry is silently skipped, not crashed on."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {
            "success": True,
            "delivered_count": 1,
            "errors": ["not a dict", {"kind": "item", "error": "real"}],
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["media_errors"] == [
        {"target_index": 0, "kind": "item", "error": "real"}
    ]


# ---------------------------------------------------------------------------
# Integration: real dispatcher output shape from ``_aggregate_results``
# ---------------------------------------------------------------------------
#
# The dispatcher wraps each Telegram fan-out in a per-target envelope:
#
#   {
#     "success": True,
#     "receivers": 2,
#     "successes": 2,
#     "failures": 0,
#     "results": [<per-receiver dict>, ...],   # ← media counts live HERE
#   }
#
# These tests use that exact shape so a future refactor of the dispatcher
# doesn't silently zero out the dashboard's ``dispatch_summary.media``
# block. Earlier versions of this file passed leaf dicts directly, which
# masked the wrong-shape read in production.


def test_summarize_drills_into_aggregated_per_receiver_dicts(tmp_data_dir) -> None:  # noqa: ARG001
    """Media counts on per-receiver leaves are summed across receivers."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    # Two targets, each with two Telegram receivers.
    results = [
        {
            "success": True,
            "receivers": 2,
            "successes": 2,
            "failures": 0,
            "results": [
                {
                    "success": True,
                    "message_id": 100,
                    "media_delivered_count": 5,
                    "media_skipped_count": 1,
                    "media_failed_count": 0,
                },
                {
                    "success": True,
                    "message_id": 101,
                    "media_delivered_count": 3,
                    "media_skipped_count": 0,
                    "media_failed_count": 0,
                },
            ],
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["media"] == {"delivered": 8, "skipped": 1, "failed": 0}


def test_summarize_collects_aggregated_media_errors_with_receiver_index(
    tmp_data_dir,  # noqa: ARG001
) -> None:
    """Per-chunk / per-item media errors carry both target AND receiver index."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {
            "success": True,
            "receivers": 1,
            "successes": 1,
            "failures": 0,
            "results": [
                {
                    "success": True,
                    "message_id": 200,
                    "media_delivered_count": 2,
                    "media_failed_count": 1,
                    "media_errors": [
                        {"kind": "chunk", "chunk": 1, "error": "Bad Request"},
                        {"kind": "item", "chunk": 1, "item_index": 2,
                         "error": "media not found"},
                    ],
                },
            ],
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["media_errors"] == [
        {"target_index": 0, "receiver_index": 0, "kind": "chunk",
         "chunk": 1, "error": "Bad Request"},
        {"target_index": 0, "receiver_index": 0, "kind": "item",
         "chunk": 1, "item_index": 2, "error": "media not found"},
    ]


def test_summarize_aggregated_target_errors_list_is_safely_ignored(
    tmp_data_dir,  # noqa: ARG001
) -> None:
    """``_aggregate_results`` stamps a flat ``errors: [str, ...]`` at the
    target level on failure. The summarizer must not try to treat the
    strings as structured sub-errors."""
    from notify_bridge_server.services.dispatch_summary import (
        summarize_dispatch_results,
    )

    results = [
        {
            "success": False,
            "receivers": 2,
            "successes": 0,
            "failures": 2,
            "error": "All receivers failed",
            "errors": ["chat_not_found", "blocked_by_user"],
            "results": [
                {"success": False, "error": "chat_not_found"},
                {"success": False, "error": "blocked_by_user"},
            ],
        },
    ]
    summary = summarize_dispatch_results(results)
    assert summary["targets_failed"] == 1
    assert summary["errors"] == [
        {"index": 0, "error": "All receivers failed"},
    ]
    # The string list at the target level is ignored — the per-receiver
    # errors are already represented by the target-level error message.
    assert "media_errors" not in summary
    assert "media" not in summary


@pytest.mark.asyncio
async def test_attach_summary_in_place_skips_when_already_set(
    tmp_data_dir,  # noqa: ARG001
) -> None:
    """Caller-set ``dispatch_summary`` wins — the same "caller pins"
    rule that ``enrich_details_with_correlation`` follows."""
    from notify_bridge_server.database.models import EventLog
    from notify_bridge_server.services.dispatch_summary import (
        attach_summary_in_place,
    )

    row = EventLog(
        event_type="assets_added",
        collection_id="abc",
        collection_name="Album",
        details={"dispatch_summary": {"pinned": True}},
    )
    attach_summary_in_place(row, [{"success": True}])
    assert row.details["dispatch_summary"] == {"pinned": True}
