#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Audit tests for dispatcher_lifecycle.archive_completed.

Verifies that fully-completed dispatches are:
  - Written to the completed/ directory as valid JSON
  - Stamped with archived_at
  - Removed from the live board
  - Left in place when incomplete or empty
  - Handled gracefully on OSError
"""
import json
import os
import time
from pathlib import Path

import pytest

import pulse.tasks.dispatcher_lifecycle as lc


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_dispatch(dispatch_id: str, statuses: list[str]) -> dict:
    tasks = [
        {"task_id": f"{dispatch_id}_t{i}", "status": s}
        for i, s in enumerate(statuses)
    ]
    return {"dispatch_id": dispatch_id, "prompt": "test", "tasks": tasks}


def _board(*dispatches: dict) -> dict:
    return {"dispatches": list(dispatches), "version": 1}


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def patch_completed_dir(tmp_path, monkeypatch):
    """Redirect COMPLETED_DIR so tests never touch the real state/completed/."""
    completed = tmp_path / "completed"
    monkeypatch.setattr(lc, "COMPLETED_DIR", completed)
    return completed


# ---------------------------------------------------------------------------
# Core happy-path tests
# ---------------------------------------------------------------------------

def test_all_done_tasks_archived(patch_completed_dir):
    """A dispatch where every task is 'done' must be archived."""
    d = _make_dispatch("d_done_001", ["done", "done", "done"])
    board = _board(d)

    result = lc.archive_completed(board)

    assert result is True
    assert board["dispatches"] == []

    archive_file = patch_completed_dir / "d_done_001.json"
    assert archive_file.exists()
    data = json.loads(archive_file.read_text(encoding="utf-8"))
    assert data["dispatch_id"] == "d_done_001"
    assert "archived_at" in data


def test_all_failed_tasks_archived(patch_completed_dir):
    """A dispatch where every task is 'failed' is also terminal and must be archived."""
    d = _make_dispatch("d_fail_001", ["failed", "failed"])
    board = _board(d)

    result = lc.archive_completed(board)

    assert result is True
    assert board["dispatches"] == []
    assert (patch_completed_dir / "d_fail_001.json").exists()


def test_mixed_done_and_failed_archived(patch_completed_dir):
    """A dispatch with a mix of 'done' and 'failed' is fully terminal — archive it."""
    d = _make_dispatch("d_mixed_001", ["done", "failed", "done"])
    board = _board(d)

    result = lc.archive_completed(board)

    assert result is True
    assert board["dispatches"] == []


# ---------------------------------------------------------------------------
# Non-terminal dispatches must stay on the board
# ---------------------------------------------------------------------------

@pytest.mark.parametrize("status", ["open", "claimed", "active", "blocked", "rejected"])
def test_incomplete_dispatch_not_archived(status, patch_completed_dir):
    """Any non-terminal task status keeps the entire dispatch on the board."""
    d = _make_dispatch("d_incomplete_001", ["done", status])
    board = _board(d)

    result = lc.archive_completed(board)

    assert result is False
    assert len(board["dispatches"]) == 1
    assert not list(patch_completed_dir.glob("*.json"))


def test_empty_task_list_not_archived(patch_completed_dir):
    """A dispatch with no tasks should remain on the board (guard against falsy all())."""
    d = {"dispatch_id": "d_empty_001", "prompt": "test", "tasks": []}
    board = _board(d)

    result = lc.archive_completed(board)

    assert result is False
    assert len(board["dispatches"]) == 1


# ---------------------------------------------------------------------------
# Board mutation tests
# ---------------------------------------------------------------------------

def test_only_completed_dispatches_removed(patch_completed_dir):
    """Mixed board: only fully-done dispatch is archived; incomplete stays."""
    done_d = _make_dispatch("d_done_002", ["done"])
    open_d = _make_dispatch("d_open_002", ["done", "open"])
    board = _board(done_d, open_d)

    result = lc.archive_completed(board)

    assert result is True
    assert len(board["dispatches"]) == 1
    assert board["dispatches"][0]["dispatch_id"] == "d_open_002"
    assert (patch_completed_dir / "d_done_002.json").exists()
    assert not (patch_completed_dir / "d_open_002.json").exists()


def test_multiple_completed_dispatches_all_archived(patch_completed_dir):
    """All fully-done dispatches in one pass are archived."""
    board = _board(
        _make_dispatch("d_a_001", ["done"]),
        _make_dispatch("d_b_001", ["failed"]),
        _make_dispatch("d_c_001", ["done", "failed"]),
    )

    result = lc.archive_completed(board)

    assert result is True
    assert board["dispatches"] == []
    assert len(list(patch_completed_dir.glob("*.json"))) == 3


def test_empty_board_returns_false(patch_completed_dir):
    """An empty board must return False (no work done)."""
    board = {"dispatches": [], "version": 1}

    result = lc.archive_completed(board)

    assert result is False


def test_no_dispatches_key_returns_false(patch_completed_dir):
    """Board without 'dispatches' key must return False gracefully."""
    board = {"version": 1}

    result = lc.archive_completed(board)

    assert result is False


# ---------------------------------------------------------------------------
# archived_at field
# ---------------------------------------------------------------------------

def test_archived_at_is_set(patch_completed_dir):
    """Archived dispatch must have a non-empty archived_at timestamp."""
    d = _make_dispatch("d_ts_001", ["done"])
    board = _board(d)
    before = time.time()

    lc.archive_completed(board)

    data = json.loads((patch_completed_dir / "d_ts_001.json").read_text("utf-8"))
    assert "archived_at" in data
    assert data["archived_at"]  # non-empty string


# ---------------------------------------------------------------------------
# Archive file content integrity
# ---------------------------------------------------------------------------

def test_archive_file_contains_all_tasks(patch_completed_dir):
    """The archived JSON must include the full tasks array."""
    d = _make_dispatch("d_content_001", ["done", "failed"])
    board = _board(d)

    lc.archive_completed(board)

    data = json.loads((patch_completed_dir / "d_content_001.json").read_text("utf-8"))
    assert len(data["tasks"]) == 2
    assert data["tasks"][0]["status"] == "done"
    assert data["tasks"][1]["status"] == "failed"


# ---------------------------------------------------------------------------
# OSError resilience
# ---------------------------------------------------------------------------

def test_oserror_on_write_keeps_dispatch_on_board(patch_completed_dir, monkeypatch):
    """If the archive write fails, the dispatch must stay on the board."""
    def _bad_open(*args, **kwargs):
        raise OSError("disk full")

    monkeypatch.setattr("builtins.open", _bad_open)

    d = _make_dispatch("d_err_001", ["done"])
    board = _board(d)

    result = lc.archive_completed(board)

    # Modified is False because nothing was successfully archived
    assert result is False
    assert len(board["dispatches"]) == 1


# ---------------------------------------------------------------------------
# Idempotency
# ---------------------------------------------------------------------------

def test_idempotent_on_already_empty_board(patch_completed_dir):
    """Calling archive_completed twice on a board that was cleared is safe."""
    d = _make_dispatch("d_idem_001", ["done"])
    board = _board(d)

    lc.archive_completed(board)
    # Second call on now-empty board
    result = lc.archive_completed(board)

    assert result is False
