from __future__ import annotations

from datetime import datetime, timedelta, timezone
import json

import pytest

from api.config import settings
from api.database import Subscription, SubscriptionPlan, SubscriptionTier
from api.models import CompleteRequest
from api.services.artifact_service import ArtifactService
from api.services.context_management_service import ContextBudgetExceeded, ContextManagementService
from api.services.tool_executor import ToolExecutor, ToolExecutionError


def _make_subscription(test_db):
    # Ensure default plans exist for this test DB.
    plan = test_db.query(SubscriptionPlan).filter_by(tier=SubscriptionTier.PRO).first()
    if plan is None:
        from api.database import seed_default_plans

        seed_default_plans(test_db)
    plan = test_db.query(SubscriptionPlan).filter_by(tier=SubscriptionTier.PRO).first()
    assert plan is not None
    now = datetime.now(timezone.utc)
    sub = Subscription(
        plan_id=plan.id,
        billing_cycle_start=now,
        billing_cycle_end=now + timedelta(days=30),
        status="active",
    )
    test_db.add(sub)
    test_db.commit()
    return sub


def test_artifact_put_and_fetch_tool_head(test_db):
    sub = _make_subscription(test_db)
    svc = ArtifactService(test_db)
    rec = svc.put_text(
        subscription_id=sub.id,
        user_id=None,
        kind="tool_result",
        text="a\nb\nc\nd\n",
        content_type="text/plain",
        source="test",
        token_estimate=4,
        retention_days=7,
    )

    te = ToolExecutor(workspace_root=".")
    out = te.execute_tool(
        "fetch_artifact",
        {"handle": rec.handle, "mode": "head", "selector": {"lines": 2}, "__subscription_id": sub.id},
    )
    assert out["status"] == "success"
    assert out["content"] == "a\nb"


def test_fetch_artifact_denies_cross_subscription_access(test_db):
    sub_a = _make_subscription(test_db)
    sub_b = _make_subscription(test_db)
    svc = ArtifactService(test_db)
    rec = svc.put_text(
        subscription_id=sub_a.id,
        user_id=None,
        kind="message",
        text="secret\n",
        content_type="text/plain",
        source="test",
        token_estimate=1,
        retention_days=7,
    )

    te = ToolExecutor(workspace_root=".")
    with pytest.raises(ToolExecutionError):
        te.execute_tool("fetch_artifact", {"handle": rec.handle, "mode": "head", "__subscription_id": sub_b.id})


def test_fetch_artifact_clamps_large_head_request(test_db):
    sub = _make_subscription(test_db)
    svc = ArtifactService(test_db)
    big = "\n".join([f"line{i}" for i in range(2000)])
    rec = svc.put_text(
        subscription_id=sub.id,
        user_id=None,
        kind="log",
        text=big,
        content_type="text/plain",
        source="test",
        token_estimate=2000,
        retention_days=7,
    )

    te = ToolExecutor(workspace_root=".")
    out = te.execute_tool(
        "fetch_artifact",
        {"handle": rec.handle, "mode": "head", "selector": {"lines": 100000}, "__subscription_id": sub.id},
    )
    # Tool clamps to <= 400 lines to prevent context explosions.
    assert out["lines"] <= 400


def test_compaction_policy_off_fails_over_budget(test_db):
    sub = _make_subscription(test_db)

    cm = ContextManagementService(test_db)
    msgs = [
        CompleteRequest.Message(role="system", content="sys"),
        CompleteRequest.Message(role="user", content=("x" * 5000)),
    ]
    with pytest.raises(ContextBudgetExceeded):
        cm.compact_if_needed(
            messages=msgs,
            artifacts=[],
            policy="off",
            model="gpt-test-small",
            token_provider=None,
            token_api_key=None,
            serving_provider="openai",
            canonical_model_id="gpt-test-small",
            routed_model_id=None,
            context_window_override=256,
            safety_margin_override=0,
            reserved_completion_tokens=64,
            capsule_spam_threshold=999,
        )


def test_fetch_artifact_bounds_output_chars_to_prevent_dump_loops(test_db):
    """
    Even if a caller requests max lines, fetch_artifact must keep content bounded
    so tool outputs don't re-externalize endlessly.
    """
    sub = _make_subscription(test_db)
    svc = ArtifactService(test_db)

    long_line = "x" * 500
    big = "\n".join([long_line for _ in range(5000)])
    rec = svc.put_text(
        subscription_id=sub.id,
        user_id=None,
        kind="log",
        text=big,
        content_type="text/plain",
        source="test",
        token_estimate=5000,
        retention_days=7,
    )

    te = ToolExecutor(workspace_root=".")
    out = te.execute_tool(
        "fetch_artifact",
        {"handle": rec.handle, "mode": "head", "selector": {"lines": 400}, "__subscription_id": sub.id},
    )
    assert out["status"] == "success"
    assert out["truncated"] is True
    assert len(out["content"]) <= int(settings.FETCH_ARTIFACT_MAX_CHARS) + len("\n...[truncated]")


def test_compaction_llm_assisted_uses_llm_summary_when_available(test_db, monkeypatch):
    sub = _make_subscription(test_db)

    cm = ContextManagementService(test_db)

    def _fake_llm_summary(**_kwargs):
        return "## Pinned State Summary\n- LLM built summary\n"

    monkeypatch.setattr(cm, "_llm_build_pinned_summary", _fake_llm_summary)

    msgs = [CompleteRequest.Message(role="system", content="sys")]
    for i in range(120):
        msgs.append(CompleteRequest.Message(role="assistant", content=f"msg{i} " + ("a" * 100)))
    msgs.append(CompleteRequest.Message(role="user", content="ok"))

    res = cm.compact_if_needed(
        messages=msgs,
        artifacts=[],
        policy="balanced",
        llm_assisted=True,
        model="gpt-test-mid",
        token_provider=None,
        token_api_key=None,
        serving_provider="openai",
        canonical_model_id=None,
        routed_model_id=None,
        context_window_override=2048,
        safety_margin_override=0,
        reserved_completion_tokens=64,
        capsule_spam_threshold=999,
    )
    assert res.compaction_triggered is True
    pinned_msgs = [m for m in res.messages if m.role == "system" and (m.content or "").startswith("## Pinned State Summary")]
    assert pinned_msgs, "Expected a pinned summary system message"
    assert "LLM built summary" in (pinned_msgs[0].content or "")


def test_compaction_llm_assisted_falls_back_deterministically_on_failure(test_db, monkeypatch):
    sub = _make_subscription(test_db)

    cm = ContextManagementService(test_db)

    def _boom(**_kwargs):
        raise RuntimeError("llm down")

    monkeypatch.setattr(cm, "_llm_build_pinned_summary", _boom)

    msgs = [CompleteRequest.Message(role="system", content="sys")]
    for i in range(120):
        msgs.append(CompleteRequest.Message(role="assistant", content=f"msg{i} " + ("a" * 100)))
    msgs.append(CompleteRequest.Message(role="user", content="ok"))

    res = cm.compact_if_needed(
        messages=msgs,
        artifacts=[],
        policy="balanced",
        llm_assisted=True,
        model="gpt-test-mid2",
        token_provider=None,
        token_api_key=None,
        serving_provider="openai",
        canonical_model_id=None,
        routed_model_id=None,
        context_window_override=2048,
        safety_margin_override=0,
        reserved_completion_tokens=64,
        capsule_spam_threshold=999,
    )
    assert res.compaction_triggered is True
    pinned_msgs = [m for m in res.messages if m.role == "system" and "## Pinned State Summary" in (m.content or "")]
    assert pinned_msgs, "Expected a pinned summary system message"
    assert "LLM built summary" not in (pinned_msgs[0].content or "")


def test_extreme_overflow_returns_best_effort_after_compaction(test_db):
    sub = _make_subscription(test_db)

    cm = ContextManagementService(test_db)
    msgs = [
        CompleteRequest.Message(role="system", content="sys"),
        CompleteRequest.Message(role="user", content=("x" * 5000)),
    ]
    res = cm.compact_if_needed(
        messages=msgs,
        artifacts=[],
        policy="balanced",
        llm_assisted=False,
        model="gpt-test-tiny",
        token_provider=None,
        token_api_key=None,
        serving_provider="openai",
        canonical_model_id=None,
        routed_model_id=None,
        context_window_override=256,
        safety_margin_override=0,
        reserved_completion_tokens=64,
        capsule_spam_threshold=999,
    )
    assert res.compaction_triggered is True
    assert "best_effort" in (res.compaction_reason or "")
    assert res.token_estimates.get("best_effort") is True

