"""Integration tests that hit real OpenRouter APIs and Docker.

Run with:
    export $(grep -E '^(OPENROUTER_API_KEY|OR_ADMIN_KEY)=' ~/.env | xargs)
    python -m pytest tests/test_integration.py -v

These tests require:
- OPENROUTER_API_KEY and OR_ADMIN_KEY env vars
- Docker daemon running (OrbStack)
- A small amount of OpenRouter credit (~$0.02 total)
"""

from __future__ import annotations

import json
import os
import subprocess
import tempfile
import time
from pathlib import Path

import pytest

# Skip entire module if keys missing
OR_KEY = os.environ.get("OPENROUTER_API_KEY", "")
ADMIN_KEY = os.environ.get("OR_ADMIN_KEY", "")
pytestmark = pytest.mark.skipif(
    not OR_KEY or not ADMIN_KEY,
    reason="OPENROUTER_API_KEY and OR_ADMIN_KEY required",
)

DOCKER_AVAILABLE = (
    subprocess.run(
        ["docker", "info"], capture_output=True, timeout=5
    ).returncode == 0
    if OR_KEY
    else False
)


# ---------------------------------------------------------------------------
# provision.py
# ---------------------------------------------------------------------------


class TestProvision:
    def test_get_account_balance(self):
        from agent_preflight.provision import get_account_balance

        result = get_account_balance(OR_KEY)
        # Returns (usage, limit, remaining) or similar
        assert result is not None
        # Should have some remaining balance
        if isinstance(result, tuple):
            assert result[2] > 0  # remaining > 0

    def test_get_available_models(self):
        from agent_preflight.provision import get_available_models

        models = get_available_models(OR_KEY)
        assert isinstance(models, dict)
        assert len(models) > 0
        # At least one well-known model should exist
        known = {"meta-llama/llama-3.3-70b-instruct", "openai/gpt-4o-mini", "anthropic/claude-haiku-4"}
        assert known & set(models.keys()), f"No known models found in {len(models)} models"

    def test_provision_and_delete_key(self):
        from agent_preflight.provision import delete_key, get_usage, provision_key_full

        # Provision a tiny key (get hash for cleanup)
        key, key_hash = provision_key_full(ADMIN_KEY, label="test-integration-cleanup", limit_usd=0.01)
        assert key.startswith("sk-or-")
        assert len(key) > 20
        assert len(key_hash) > 0

        # Check usage (should be 0 for a fresh key)
        usage = get_usage(key)
        assert usage is not None
        assert usage == 0.0 or usage < 0.001

        # Clean up by hash
        deleted = delete_key(ADMIN_KEY, key_hash)
        assert deleted is True

    def test_get_usage_invalid_key(self):
        from agent_preflight.provision import get_usage

        usage = get_usage("sk-or-v1-invalid-key-that-doesnt-exist")
        assert usage is None


# ---------------------------------------------------------------------------
# budget.py — poll real usage from a provisioned key
# ---------------------------------------------------------------------------


class TestBudgetTracker:
    def test_poll_fresh_key(self):
        from agent_preflight.budget import BudgetTracker
        from agent_preflight.provision import delete_key, provision_key_full

        key, key_hash = provision_key_full(ADMIN_KEY, label="test-budget-tracker", limit_usd=0.01)
        try:
            tracker = BudgetTracker(api_key=key, limit_usd=0.01, poll_interval_s=0)
            status = tracker.poll(force=True)
            assert status.used < 0.001
            assert status.limit == 0.01
            assert status.remaining > 0
            assert not status.exceeded

            msg = tracker.status_message(force_poll=True)
            assert "remaining" in msg
            assert "$0.01" in msg
        finally:
            delete_key(ADMIN_KEY, key_hash)

    def test_exceeded_callback(self):
        from agent_preflight.budget import BudgetTracker

        fired = []
        # Use a key with 0 limit so it's "exceeded" immediately
        tracker = BudgetTracker(
            api_key=OR_KEY,  # will show real usage
            limit_usd=0.0,   # anything > 0 usage exceeds this
            poll_interval_s=0,
            on_exceeded=lambda: fired.append(True),
        )
        tracker.poll(force=True)
        # If account has any usage at all, callback should fire
        # (our account has $5+ usage)
        assert len(fired) == 1

    def test_detailed_mode_format(self):
        from agent_preflight.budget import BudgetTracker
        from agent_preflight.provision import delete_key, provision_key_full

        key, key_hash = provision_key_full(ADMIN_KEY, label="test-budget-detailed", limit_usd=0.01)
        try:
            tracker = BudgetTracker(api_key=key, limit_usd=0.01, mode="detailed")
            msg = tracker.status_message(force_poll=True)
            assert "remaining" in msg
            assert "No cost data yet" in msg  # fresh key, no actions
        finally:
            delete_key(ADMIN_KEY, key_hash)


# ---------------------------------------------------------------------------
# preflight.py — real preflight checks
# ---------------------------------------------------------------------------


class TestPreflight:
    """preflight() returns a list of failure strings (empty = all passed)."""

    def test_balance_and_math_checks(self):
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["meta-llama/llama-3.3-70b-instruct"],
            budget_per_model=0.01,
            checks=["balance", "math"],
            raise_on_failure=False,
        )
        assert failures == [], f"Expected no failures, got: {failures}"

    def test_model_check(self):
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["meta-llama/llama-3.3-70b-instruct"],
            budget_per_model=0.01,
            checks=["models"],
            raise_on_failure=False,
        )
        assert failures == []

    def test_model_check_nonexistent(self):
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["nonexistent/fake-model-xyz"],
            budget_per_model=0.01,
            checks=["models"],
            raise_on_failure=False,
        )
        assert len(failures) > 0
        assert any("nonexistent" in f for f in failures)

    def test_key_smoke_check(self):
        """Provision a real key and make a real API call."""
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["meta-llama/llama-3.3-70b-instruct"],
            budget_per_model=0.01,
            checks=["key_smoke"],
            raise_on_failure=False,
        )
        assert failures == [], f"key_smoke failed: {failures}"

    def test_raise_on_failure(self):
        from agent_preflight.preflight import PreflightError, preflight

        with pytest.raises(PreflightError):
            preflight(
                admin_key=ADMIN_KEY,
                or_key=OR_KEY,
                models=["nonexistent/fake-model-xyz"],
                budget_per_model=0.01,
                checks=["models"],
                raise_on_failure=True,
            )

    def test_math_check_insufficient_balance(self):
        """Budget > balance should fail math check."""
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["meta-llama/llama-3.3-70b-instruct"],
            budget_per_model=999999.0,
            checks=["math"],
            raise_on_failure=False,
        )
        assert len(failures) > 0
        assert any("Insufficient" in f for f in failures)


# ---------------------------------------------------------------------------
# docker.py — build and run a trivial container
# ---------------------------------------------------------------------------


@pytest.mark.skipif(not DOCKER_AVAILABLE, reason="Docker not available")
class TestDocker:
    def test_image_exists(self):
        from agent_preflight.docker import image_exists

        # alpine is commonly cached; pull if needed
        subprocess.run(["docker", "pull", "alpine:latest"], capture_output=True, timeout=60)
        assert image_exists("alpine:latest")
        assert not image_exists("nonexistent-image-xyz:latest")

    def test_build_image(self):
        from agent_preflight.docker import build_image, image_exists

        with tempfile.TemporaryDirectory() as tmp:
            dockerfile = Path(tmp) / "Dockerfile"
            dockerfile.write_text("FROM alpine:latest\nCMD echo hello\n")
            build_image("ap-test-build:latest", dockerfile=dockerfile, context=tmp)
            assert image_exists("ap-test-build:latest")
            # Clean up
            subprocess.run(["docker", "rmi", "ap-test-build:latest"], capture_output=True)

    def test_build_image_bad_dockerfile_raises(self):
        from agent_preflight.docker import build_image

        with tempfile.TemporaryDirectory() as tmp:
            dockerfile = Path(tmp) / "Dockerfile"
            dockerfile.write_text("FROM nonexistent-base-image-xyz:latest\n")
            with pytest.raises(RuntimeError, match="Docker build failed"):
                build_image("ap-test-bad:latest", dockerfile=dockerfile, context=tmp)

    def test_remove_container_noop(self):
        """remove_container on a nonexistent container should not raise."""
        from agent_preflight.docker import remove_container

        remove_container("ap-nonexistent-container-xyz")  # should not raise


# ---------------------------------------------------------------------------
# Full mini end-to-end: provision key, run a container, check results
# ---------------------------------------------------------------------------


@pytest.mark.skipif(not DOCKER_AVAILABLE, reason="Docker not available")
class TestEndToEnd:
    """Minimal e2e: build a tiny agent image that emits events, run it."""

    @pytest.fixture(autouse=True)
    def _setup_image(self):
        """Build a minimal 'agent' that emits structured events then exits."""
        self.image_name = "ap-test-agent:latest"
        with tempfile.TemporaryDirectory() as tmp:
            dockerfile = Path(tmp) / "Dockerfile"
            agent_script = Path(tmp) / "agent.sh"
            agent_script.write_text(
                '#!/bin/sh\n'
                'echo \'{"type":"agent_start","model_id":"test"}\'\n'
                'echo \'{"type":"tool_execution_start","toolName":"bash"}\'\n'
                'sleep 0.5\n'
                'echo \'{"type":"tool_execution_end","toolName":"bash","usage":{"prompt_tokens":100,"completion_tokens":20,"prompt_tokens_details":{"cached_tokens":80}}}\'\n'
                'echo \'{"type":"agent_end","status":"success"}\'\n'
            )
            agent_script.chmod(0o755)
            dockerfile.write_text(
                f"FROM alpine:latest\n"
                f"COPY agent.sh /agent.sh\n"
                f"CMD [\"/agent.sh\"]\n"
            )
            subprocess.run(
                ["docker", "build", "-t", self.image_name, "-f", str(dockerfile), tmp],
                capture_output=True, check=True, timeout=60,
            )
        yield
        subprocess.run(["docker", "rmi", self.image_name], capture_output=True)

    def test_run_container_captures_events_and_cache(self):
        """Run the fake agent and verify events, cache stats, and summary.

        run_container provisions its own sub-key internally via admin_key,
        so we just pass ADMIN_KEY directly.
        """
        from agent_preflight.docker import run_container

        with tempfile.TemporaryDirectory() as tmp:
            run_dir = Path(tmp) / "test-run"
            run_dir.mkdir()

            result = run_container(
                image_name=self.image_name,
                model_id="test/fake-model",
                alias="e2e-test",
                admin_key=ADMIN_KEY,
                budget_usd=0.01,
                run_dir=run_dir,
                run_id="test-e2e-run",
                idle_timeout_s=10.0,
            )

            # Basic result checks
            assert result.alias == "e2e-test"
            assert result.model_id == "test/fake-model"
            assert result.tool_calls == 1
            assert result.events_count >= 4  # start, tool_start, tool_end, agent_end

            # Cache stats should have been extracted from the usage event
            assert result.cache.request_count == 1
            assert result.cache.total_prompt_tokens == 100
            assert result.cache.cached_tokens == 80
            assert result.cache.hit_rate == pytest.approx(0.8)

            # summary.json should exist and be valid
            summary_path = run_dir / "e2e-test" / "summary.json"
            assert summary_path.exists()
            summary = json.loads(summary_path.read_text())
            assert summary["tool_calls"] == 1
            assert summary["cache"]["cached_tokens"] == 80
            assert summary["cache"]["hit_rate"] == 0.8

            # events.jsonl should exist
            events_path = run_dir / "e2e-test" / "events.jsonl"
            assert events_path.exists()
            from agent_preflight.events import EventLogger
            events = EventLogger.read(events_path)
            assert len(events) >= 4
            types = [e["type"] for e in events]
            assert "agent_start" in types
            assert "tool_execution_start" in types
            assert "tool_execution_end" in types
            assert "agent_end" in types

    def test_on_event_callback_fires(self):
        """Verify on_event callback receives events."""
        from agent_preflight.docker import run_container

        captured = []

        with tempfile.TemporaryDirectory() as tmp:
            run_dir = Path(tmp) / "callback-run"
            run_dir.mkdir()

            run_container(
                image_name=self.image_name,
                model_id="test/fake-model",
                alias="callback-test",
                admin_key=ADMIN_KEY,
                budget_usd=0.01,
                run_dir=run_dir,
                run_id="test-callback-run",
                on_event=lambda e: captured.append(e),
                idle_timeout_s=10.0,
            )

            assert len(captured) >= 4
            assert any(e.get("type") == "agent_end" for e in captured)


# ---------------------------------------------------------------------------
# status.py — container_running with real Docker
# ---------------------------------------------------------------------------


@pytest.mark.skipif(not DOCKER_AVAILABLE, reason="Docker not available")
class TestStatusDocker:
    def test_container_running_returns_false_for_nonexistent(self):
        from agent_preflight.status import container_running

        assert not container_running("ap-nonexistent-model")

    def test_container_running_detects_active_container(self):
        """Start a real container and verify detection.

        container_running(alias) filters for name=ap-{alias}- so a container
        named "ap-mymodel-abc12345" is found with alias="mymodel".
        """
        from agent_preflight.status import container_running

        name = "ap-statustest-abc12345"
        subprocess.run(["docker", "rm", "-f", name], capture_output=True)
        subprocess.run(
            ["docker", "run", "-d", "--name", name, "alpine:latest", "sleep", "30"],
            capture_output=True, check=True,
        )
        try:
            assert container_running("statustest")
        finally:
            subprocess.run(["docker", "rm", "-f", name], capture_output=True)


# ---------------------------------------------------------------------------
# cache_smoke preflight check (if available)
# ---------------------------------------------------------------------------


class TestCacheSmoke:
    @pytest.mark.slow
    def test_cache_smoke_check(self):
        """Run the cache_smoke preflight check against a real Anthropic model.

        This makes 2 real API calls with explicit Anthropic cache control and
        asserts the preflight smoke test passes.
        Marked slow because it takes a few seconds.
        """
        from agent_preflight.preflight import preflight

        failures = preflight(
            admin_key=ADMIN_KEY,
            or_key=OR_KEY,
            models=["anthropic/claude-haiku-4"],
            budget_per_model=0.01,
            checks=["cache_smoke"],
            raise_on_failure=False,
        )
        assert failures == [], f"cache_smoke failed: {failures}"
