"""Tests for BenchflowBackend output contract.

Verifies that _to_trial_result produces TrialResult objects that satisfy
the side_info contract expected by executor._process_result, scoring.py,
and proposer.py — without requiring the benchflow SDK to be installed.
"""

from __future__ import annotations

import datetime
from dataclasses import dataclass, field
from types import SimpleNamespace

import pytest

from clawbench.backend import Backend, TrialResult, BenchflowBackend, _acp_to_atif, _build_service_hooks, _format_acp_trajectory
from clawbench.scoring import extract_score, validate_side_info


# ---------------------------------------------------------------------------
# Fake RunResult (mirrors benchflow SDK shape without importing it)
# ---------------------------------------------------------------------------


@dataclass
class FakeRunResult:
    rewards: dict | None = None
    error: str | None = None
    trajectory: list[dict] = field(default_factory=list)
    started_at: datetime.datetime | None = None
    finished_at: datetime.datetime | None = None


def _make_backend(**kwargs) -> BenchflowBackend:
    """Create a BenchflowBackend without calling setup() (no SDK needed)."""
    return BenchflowBackend(**kwargs)


# ---------------------------------------------------------------------------
# Factory & protocol
# ---------------------------------------------------------------------------


class TestFactory:
    def test_create_benchflow_backend(self):
        backend = BenchflowBackend()
        assert isinstance(backend, BenchflowBackend)

    def test_kwargs_forwarded(self):
        backend = BenchflowBackend(
            agent_name="test-agent",
            model_name="test-model",
            keep_trials=True,
        )
        assert backend.agent_name == "test-agent"
        assert backend.model_name == "test-model"
        assert backend.keep_trials is True


class TestProtocol:
    def test_satisfies_backend_protocol(self):
        backend = BenchflowBackend()
        assert isinstance(backend, Backend)

    def test_setup_idempotent(self):
        backend = BenchflowBackend()
        # Simulate SDK already initialized
        backend._sdk = SimpleNamespace()
        backend.setup()  # should be a no-op
        assert isinstance(backend._sdk, SimpleNamespace)

    def test_cleanup_noop(self):
        BenchflowBackend().cleanup()


# ---------------------------------------------------------------------------
# _to_trial_result — output contract
# ---------------------------------------------------------------------------


class TestToTrialResultCompleted:
    """Completed trial with rewards."""

    def setup_method(self):
        self.backend = _make_backend()
        self.run_result = FakeRunResult(
            rewards={"score": 0.8, "partial_credit": 0.5},
            trajectory=[
                {"type": "tool_call", "kind": "bash", "title": "ls", "status": "ok"},
                {"type": "agent_message", "text": "Found files"},
            ],
            started_at=datetime.datetime(2026, 1, 1, 0, 0, 0),
            finished_at=datetime.datetime(2026, 1, 1, 0, 1, 30),
        )
        self.result = self.backend._to_trial_result(
            self.run_result, "my-task", "skill content"
        )

    def test_returns_trial_result(self):
        assert isinstance(self.result, TrialResult)

    def test_score_extracted(self):
        assert self.result.score == 0.8

    def test_score_clamped(self):
        run_result = FakeRunResult(rewards={"score": 5.0}, trajectory=[])
        result = self.backend._to_trial_result(run_result, "t", "")
        assert result.score == 1.0

    def test_feedback_status_completed(self):
        assert self.result.side_info["Feedback"]["Status"] == "completed"

    def test_feedback_rewards_is_dict(self):
        rewards = self.result.side_info["Feedback"]["Rewards"]
        assert isinstance(rewards, dict)
        assert rewards["score"] == 0.8

    def test_rewards_pass_extract_score(self):
        """Rewards dict satisfies scoring.extract_score() contract."""
        rewards = self.result.side_info["Feedback"]["Rewards"]
        assert extract_score(rewards) == 0.8

    def test_scores_convenience_key(self):
        assert self.result.side_info["scores"] == {"score": 0.8, "partial_credit": 0.5}

    def test_generated_outputs_is_dict(self):
        gen = self.result.side_info["Generated Outputs"]
        assert isinstance(gen, dict)
        assert "Agent Trace" in gen

    def test_input_has_task_name(self):
        assert self.result.side_info["Input"]["task_name"] == "my-task"

    def test_timing_populated(self):
        assert self.result.side_info["Input"]["timing"]["total"] == 90.0
        assert self.result.side_info["Input"]["duration_sec"] == 90.0

    def test_passes_validate_side_info(self):
        warnings = validate_side_info(self.result.side_info, "my-task")
        assert warnings == []


class TestToTrialResultError:
    """Trial that errored."""

    def setup_method(self):
        self.backend = _make_backend()
        self.result = self.backend._to_trial_result(
            FakeRunResult(error="container OOM", trajectory=[]),
            "fail-task",
            "",
        )

    def test_score_zero(self):
        assert self.result.score == 0.0

    def test_feedback_status_error(self):
        assert self.result.side_info["Feedback"]["Status"] == "trial_error"

    def test_feedback_error_message(self):
        assert self.result.side_info["Feedback"]["Error"] == "container OOM"

    def test_no_scores_key(self):
        assert "scores" not in self.result.side_info

    def test_passes_validate_side_info(self):
        warnings = validate_side_info(self.result.side_info, "fail-task")
        assert warnings == []


class TestToTrialResultNoVerifier:
    """Trial with no rewards and no error."""

    def setup_method(self):
        self.backend = _make_backend()
        self.result = self.backend._to_trial_result(
            FakeRunResult(trajectory=[]), "empty-task", ""
        )

    def test_score_zero(self):
        assert self.result.score == 0.0

    def test_feedback_status(self):
        assert self.result.side_info["Feedback"]["Status"] == "no_verifier_result"

    def test_passes_validate_side_info(self):
        warnings = validate_side_info(self.result.side_info, "empty-task")
        assert warnings == []


class TestToTrialResultRewardKey:
    """Rewards dict with 'reward' instead of 'score'."""

    def test_reward_key_extracted(self):
        backend = _make_backend()
        result = backend._to_trial_result(
            FakeRunResult(rewards={"reward": 0.6}, trajectory=[]),
            "t", "",
        )
        assert result.score == 0.6
        # Also passes extract_score
        assert extract_score(result.side_info["Feedback"]["Rewards"]) == 0.6


class TestTrajectoryStore:
    """Trajectory store is populated when provided."""

    def test_store_appended(self):
        store = []
        backend = _make_backend(trajectory_store=store)
        backend._to_trial_result(
            FakeRunResult(
                rewards={"score": 1.0},
                trajectory=[
                    {"type": "tool_call", "kind": "bash", "title": "echo hi"},
                    {"type": "agent_message", "text": "done"},
                ],
                started_at=datetime.datetime(2026, 1, 1),
                finished_at=datetime.datetime(2026, 1, 1, 0, 0, 10),
            ),
            "store-task",
            "my skill",
        )
        assert len(store) == 1
        entry = store[0]
        assert entry["task_name"] == "store-task"
        assert entry["score"] == 1.0
        assert len(entry["steps"]) == 2

    def test_store_with_lock(self):
        import threading
        store = []
        lock = threading.Lock()
        backend = _make_backend(trajectory_store=store, trajectory_lock=lock)
        backend._to_trial_result(
            FakeRunResult(rewards={"score": 0.5}, trajectory=[]),
            "t", "",
        )
        assert len(store) == 1


# ---------------------------------------------------------------------------
# _format_acp_trajectory
# ---------------------------------------------------------------------------


class TestToTrialResultNoneTrajectory:
    """RunResult.trajectory can be None."""

    def test_none_trajectory_handled(self):
        backend = _make_backend()
        result = backend._to_trial_result(
            FakeRunResult(rewards={"score": 0.5}, trajectory=None),
            "t", "",
        )
        assert result.side_info["Generated Outputs"]["Agent Trace"] == "(no trajectory)"


class TestBuildServiceHooks:
    """_build_service_hooks auto-detects claw-* services from Dockerfile."""

    def test_no_dockerfile(self, tmp_path):
        """No hooks when no Dockerfile exists."""
        assert _build_service_hooks(str(tmp_path)) == []

    def test_docker_compose_skipped(self, tmp_path):
        """No hooks when docker-compose.yaml exists (services managed externally)."""
        env_dir = tmp_path / "environment"
        env_dir.mkdir()
        (env_dir / "Dockerfile").write_text("FROM ubuntu\nRUN apt-get install claw-gmail")
        (env_dir / "docker-compose.yaml").write_text("version: '3'")
        assert _build_service_hooks(str(tmp_path)) == []

    def test_detects_services(self, tmp_path):
        """Hooks returned when claw-* services found in Dockerfile."""
        env_dir = tmp_path / "environment"
        env_dir.mkdir()
        (env_dir / "Dockerfile").write_text(
            "FROM ubuntu\nRUN pip install claw-gmail claw-slack"
        )
        hooks = _build_service_hooks(str(tmp_path))
        assert len(hooks) == 1  # one combined hook function
        assert callable(hooks[0])

    def test_no_match(self, tmp_path):
        """No hooks when Dockerfile doesn't mention any claw-* services."""
        env_dir = tmp_path / "environment"
        env_dir.mkdir()
        (env_dir / "Dockerfile").write_text("FROM ubuntu\nRUN echo hello")
        assert _build_service_hooks(str(tmp_path)) == []


class TestFormatAcpTrajectory:
    def test_empty(self):
        assert _format_acp_trajectory([]) == "(no trajectory)"

    def test_tool_call(self):
        text = _format_acp_trajectory([
            {"type": "tool_call", "kind": "bash", "title": "ls -la", "status": "ok"}
        ])
        assert "[bash] ls -la" in text
        assert "ok" in text

    def test_agent_message(self):
        text = _format_acp_trajectory([
            {"type": "agent_message", "text": "Hello world"}
        ])
        assert "[Message]: Hello world" in text

    def test_truncation(self):
        events = [{"type": "agent_message", "text": "x" * 500} for _ in range(50)]
        text = _format_acp_trajectory(events, max_chars=100)
        assert "truncated" in text


class TestAcpToAtif:
    def test_basic_structure(self):
        atif = _acp_to_atif([], "gemini-cli", "flash")
        assert atif["schema_version"] == "ATIF-v1.6"
        assert atif["agent"]["name"] == "gemini-cli"
        assert atif["steps"] == []

    def test_tool_call(self):
        events = [{"type": "tool_call", "tool_call_id": "t1", "kind": "bash", "title": "ls", "status": "completed"}]
        atif = _acp_to_atif(events, "gemini-cli", "flash")
        step = atif["steps"][0]
        assert step["tool_calls"][0]["function_name"] == "bash"
        assert step["source"] == "agent"

    def test_agent_message(self):
        events = [{"type": "agent_message", "text": "Done!"}]
        atif = _acp_to_atif(events, "gemini-cli", "flash")
        assert atif["steps"][0]["message"] == "Done!"

    def test_agent_thought(self):
        events = [{"type": "agent_thought", "text": "Thinking..."}]
        atif = _acp_to_atif(events, "gemini-cli", "flash")
        assert atif["steps"][0]["source"] == "agent_thought"

    def test_mixed_events(self):
        events = [
            {"type": "tool_call", "kind": "bash", "title": "ls"},
            {"type": "agent_message", "text": "Here are the files"},
            {"type": "agent_thought", "text": "Need to check more"},
            {"type": "tool_call", "kind": "bash", "title": "cat foo.txt"},
        ]
        atif = _acp_to_atif(events, "test", "model")
        assert len(atif["steps"]) == 4
        assert [s["step_id"] for s in atif["steps"]] == [1, 2, 3, 4]
