from __future__ import annotations

import json
from collections.abc import Generator
from datetime import UTC, datetime, timedelta

import pytest
from auth_helpers import install_legacy_header_auth_override
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import get_db
from app.api.v1.endpoints.agent_runs import router as agent_runs_router
from app.db.base import Base
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.services.agent_runs import AgentRunService
from app.services.finance_dashboard_scope import (
    FINANCE_DASHBOARD_TASK_TYPE,
    resolve_finance_dashboard_data_scope,
)


def _session_factory() -> sessionmaker[Session]:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    return sessionmaker(bind=engine, autoflush=False, autocommit=False)


def _run(
    *,
    run_id: str,
    started_at: datetime,
    route_tenant_id: str | None,
    ontology_tenant_id: str | None,
    status: str = "succeeded",
    error_message: str | None = None,
) -> AgentRun:
    route_json: dict[str, object] = {"stage": "tenant-security-test"}
    ontology_json: dict[str, object] = {
        "scenario": "expense",
        "intent": "query",
    }
    if route_tenant_id is not None:
        route_json["tenant_id"] = route_tenant_id
    if ontology_tenant_id is not None:
        ontology_json["tenant_id"] = ontology_tenant_id
    return AgentRun(
        run_id=run_id,
        agent="orchestrator",
        source="user_message",
        user_id=f"user-{run_id}",
        ontology_json=ontology_json,
        route_json=route_json,
        permission_level="read",
        status=status,
        result_summary=f"summary-{run_id}",
        error_message=error_message,
        started_at=started_at,
        finished_at=started_at + timedelta(seconds=1),
    )


def _seed_runs(db: Session) -> None:
    now = datetime.now(UTC)
    tenant_a = _run(
        run_id="run-normal-tenant-a",
        started_at=now - timedelta(minutes=4),
        route_tenant_id="tenant-a",
        ontology_tenant_id="tenant-a",
    )
    tenant_b = _run(
        run_id="run-normal-tenant-b",
        started_at=now - timedelta(minutes=3),
        route_tenant_id="tenant-b",
        ontology_tenant_id="tenant-b",
        status="failed",
        error_message="tenant-b-run-secret",
    )
    legacy_unscoped = _run(
        run_id="run-normal-unscoped",
        started_at=now - timedelta(minutes=2),
        route_tenant_id=None,
        ontology_tenant_id=None,
        status="failed",
        error_message="legacy-unscoped-secret",
    )
    partial_scope = _run(
        run_id="run-normal-partial-scope",
        started_at=now - timedelta(seconds=90),
        route_tenant_id="tenant-a",
        ontology_tenant_id=None,
        status="failed",
        error_message="partial-scope-secret",
    )
    conflicting = _run(
        run_id="run-normal-conflicting",
        started_at=now - timedelta(minutes=1),
        route_tenant_id="tenant-a",
        ontology_tenant_id="tenant-b",
        status="failed",
        error_message="conflicting-scope-secret",
    )
    finance_scope = resolve_finance_dashboard_data_scope("tenant-a")
    finance_snapshot = _run(
        run_id="run-finance-tenant-a",
        started_at=now,
        route_tenant_id="tenant-a",
        ontology_tenant_id="tenant-a",
        status="failed",
        error_message="finance-snapshot-secret",
    )
    finance_snapshot.route_json.update(
        {
            "task_type": FINANCE_DASHBOARD_TASK_TYPE,
            "data_scope": finance_scope,
        }
    )
    finance_snapshot.ontology_json["data_scope"] = finance_scope
    db.add_all(
        [
            tenant_a,
            tenant_b,
            legacy_unscoped,
            partial_scope,
            conflicting,
            finance_snapshot,
        ]
    )
    db.flush()
    db.add_all(
        [
            AgentToolCall(
                run_id=tenant_a.run_id,
                tool_type="database",
                tool_name="tenant-a.tool",
                request_json={"private": "tenant-a-request"},
                response_json={"private": "tenant-a-response"},
                status="succeeded",
                duration_ms=3,
            ),
            AgentToolCall(
                run_id=tenant_b.run_id,
                tool_type="database",
                tool_name="tenant-b.tool",
                request_json={"private": "tenant-b-request-secret"},
                response_json={"private": "tenant-b-response-secret"},
                status="failed",
                duration_ms=5,
                error_message="tenant-b-tool-secret",
            ),
            SemanticParseLog(
                run_id=tenant_a.run_id,
                user_id="tenant-a-user",
                raw_query="tenant-a-raw-query",
                scenario="expense",
                intent="query",
                confidence=0.9,
            ),
            SemanticParseLog(
                run_id=tenant_b.run_id,
                user_id="tenant-b-user",
                raw_query="tenant-b-raw-query-secret",
                scenario="expense",
                intent="query",
                confidence=0.9,
            ),
        ]
    )
    db.commit()


def _client(
    session_factory: sessionmaker[Session],
) -> TestClient:
    app = FastAPI()
    app.include_router(agent_runs_router)
    install_legacy_header_auth_override(app)

    def override_db() -> Generator[Session, None, None]:
        with session_factory() as db:
            yield db

    app.dependency_overrides[get_db] = override_db
    return TestClient(app)


def _headers(tenant_id: str, *, is_admin: bool = False) -> dict[str, str]:
    return {
        "X-Auth-Username": f"user-{tenant_id}",
        "X-Auth-Role-Codes": "user",
        "X-Auth-Is-Admin": "true" if is_admin else "false",
        "X-Auth-Tenant-Id": tenant_id,
    }


def test_agent_run_endpoints_fail_closed_for_normal_cross_tenant_runs() -> None:
    session_factory = _session_factory()
    with session_factory() as db:
        _seed_runs(db)
    client = _client(session_factory)

    tenant_a_list = client.get(
        "/agent-runs",
        params={"limit": 1},
        headers=_headers("tenant-a"),
    )
    tenant_a_summary = client.get(
        "/agent-runs/summary",
        params={"limit": 1},
        headers=_headers("tenant-a"),
    )
    tenant_b_list = client.get(
        "/agent-runs",
        headers=_headers("tenant-b"),
    )

    assert tenant_a_list.status_code == 200
    assert [item["run_id"] for item in tenant_a_list.json()] == ["run-normal-tenant-a"]
    assert tenant_a_summary.status_code == 200
    assert tenant_a_summary.json()["total_runs"] == 1
    assert tenant_a_summary.json()["failed_runs"] == 0
    assert tenant_a_summary.json()["recent_errors"] == []
    assert tenant_b_list.status_code == 200
    assert [item["run_id"] for item in tenant_b_list.json()] == ["run-normal-tenant-b"]

    tenant_a_payload = json.dumps(
        [tenant_a_list.json(), tenant_a_summary.json()],
        ensure_ascii=False,
    )
    assert "tenant-b" not in tenant_a_payload
    assert "legacy-unscoped-secret" not in tenant_a_payload
    assert "partial-scope-secret" not in tenant_a_payload
    assert "conflicting-scope-secret" not in tenant_a_payload
    assert "finance-snapshot-secret" not in tenant_a_payload

    own_detail = client.get(
        "/agent-runs/run-normal-tenant-a",
        headers=_headers("tenant-a"),
    )
    assert own_detail.status_code == 200
    assert own_detail.json()["tool_calls"][0]["request_json"] == {"private": "tenant-a-request"}
    assert own_detail.json()["semantic_parse"]["raw_query"] == "tenant-a-raw-query"

    hidden_run_ids = [
        "run-normal-tenant-b",
        "run-normal-unscoped",
        "run-normal-partial-scope",
        "run-normal-conflicting",
    ]
    for run_id in hidden_run_ids:
        response = client.get(
            f"/agent-runs/{run_id}",
            headers=_headers("tenant-a", is_admin=True),
        )
        assert response.status_code == 404
        response_body = json.dumps(response.json(), ensure_ascii=False)
        assert "tenant-b-request-secret" not in response_body
        assert "tenant-b-response-secret" not in response_body
        assert "tenant-b-raw-query-secret" not in response_body


def test_agent_run_creation_stamps_both_payloads_and_rejects_conflicts() -> None:
    session_factory = _session_factory()
    with session_factory() as db:
        service = AgentRunService(db)
        created = service.create_run(
            agent="orchestrator",
            source="user_message",
            tenant_id="tenant-a",
            ontology_json={"scenario": "expense"},
            route_json={"stage": "created"},
            status="running",
        )

        assert created.ontology_json["tenant_id"] == "tenant-a"
        assert created.route_json["tenant_id"] == "tenant-a"

        updated = service.update_run(
            created.run_id,
            ontology_json={"scenario": "expense", "intent": "query"},
            route_json={"stage": "finished"},
            status="succeeded",
        )
        assert updated.ontology_json["tenant_id"] == "tenant-a"
        assert updated.route_json["tenant_id"] == "tenant-a"

        with pytest.raises(ValueError, match="tenant_id 与业务上下文冲突"):
            service.create_run(
                agent="orchestrator",
                source="user_message",
                tenant_id="tenant-a",
                route_json={"tenant_id": "tenant-b"},
                status="running",
            )
