from __future__ import annotations

from collections.abc import Generator
from datetime import UTC, datetime, timedelta
from decimal import Decimal

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

from app.api.deps import CurrentUserContext, get_db
from app.api.v1.endpoints.agent_runs import router as agent_runs_router
from app.api.v1.endpoints.analytics import router as analytics_router
from app.db.base import Base
from app.models.agent_run import AgentRun, AgentToolCall
from app.models.budget import BudgetAllocation
from app.models.expense_case import ExpenseCase, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim
from app.services.finance_dashboard import FinanceDashboardService
from app.services.finance_dashboard_access_policy import FinanceDashboardAccessPolicy
from app.services.finance_dashboard_scope import resolve_finance_dashboard_data_scope
from app.services.finance_dashboard_snapshot import (
    FINANCE_DASHBOARD_TASK_TYPE,
    FinanceDashboardSnapshotService,
)


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 _claim(
    *,
    claim_id: str,
    claim_no: str,
    amount: str,
    tenant_id: str = "default",
) -> ExpenseClaim:
    now = datetime.now(UTC)
    return ExpenseClaim(
        id=claim_id,
        tenant_id=tenant_id,
        claim_no=claim_no,
        employee_name=f"employee-{claim_id}",
        department_name="财务部",
        expense_type="travel",
        reason="tenant scope test",
        location="上海",
        amount=Decimal(amount),
        invoice_count=1,
        occurred_at=now - timedelta(hours=2),
        submitted_at=now - timedelta(hours=1),
        status="paid",
        approval_stage="payment",
        risk_flags_json=[],
        hermes_risk_flag=False,
        created_at=now - timedelta(hours=2),
        updated_at=now - timedelta(minutes=30),
    )


def _link_claim(db: Session, claim: ExpenseClaim, *, tenant_id: str) -> None:
    expense_case = ExpenseCase(
        id=f"case-{claim.id}",
        tenant_id=tenant_id,
        case_no=f"CASE-{claim.claim_no}",
        scene_code="travel",
        title=f"{claim.claim_no} case",
        current_stage="completed",
        status="completed",
    )
    db.add(expense_case)
    db.flush()
    db.add(
        ExpenseCaseLink(
            tenant_id=tenant_id,
            expense_case_id=expense_case.id,
            resource_type="expense_claim",
            resource_id=claim.id,
            relation_type="reimbursement",
        )
    )


def _seed_tenant_claim(
    db: Session,
    *,
    tenant_id: str,
    claim_id: str,
    claim_no: str,
    amount: str,
) -> ExpenseClaim:
    claim = _claim(
        claim_id=claim_id,
        claim_no=claim_no,
        amount=amount,
        tenant_id=tenant_id,
    )
    db.add(claim)
    db.flush()
    _link_claim(db, claim, tenant_id=tenant_id)
    return claim


def _seed_finance_snapshot_run(
    db: Session,
    *,
    run_id: str,
    tenant_id: str | None,
    amount: str,
    route_data_scope: str | None = None,
) -> None:
    route_json = {
        "task_type": FINANCE_DASHBOARD_TASK_TYPE,
        "snapshot_key": f"snapshot-{run_id}",
        "snapshot_payload": {
            "tenant_marker": tenant_id or "legacy-unscoped",
            "amount": amount,
        },
    }
    ontology_json = {"scenario": "finance_dashboard"}
    if tenant_id is not None:
        expected_scope = resolve_finance_dashboard_data_scope(tenant_id)
        route_json["tenant_id"] = tenant_id
        route_json["data_scope"] = route_data_scope or expected_scope
        ontology_json["tenant_id"] = tenant_id
        ontology_json["data_scope"] = expected_scope

    db.add(
        AgentRun(
            run_id=run_id,
            agent="hermes",
            source="system_event",
            user_id="digital_employee",
            ontology_json=ontology_json,
            route_json=route_json,
            permission_level="read",
            status="succeeded",
            result_summary=f"sensitive amount {amount}",
            started_at=datetime.now(UTC),
            finished_at=datetime.now(UTC),
        )
    )
    db.flush()
    db.add(
        AgentToolCall(
            run_id=run_id,
            tool_type="database",
            tool_name="digital_employee.finance_dashboard.snapshot",
            request_json={"tenant_id": tenant_id, "amount": amount},
            response_json={"secret_amount": amount},
            status="succeeded",
            duration_ms=1,
        )
    )


def test_finance_dashboard_isolates_claims_and_hides_legacy_budget_outside_default() -> None:
    now = datetime.now(UTC)
    session_factory = _session_factory()

    with session_factory() as db:
        db.add(_claim(claim_id="legacy-default", claim_no="CLM-LEGACY-001", amount="100.00"))
        _seed_tenant_claim(
            db,
            tenant_id="tenant-a",
            claim_id="claim-a",
            claim_no="CLM-TENANT-A-001",
            amount="200.00",
        )
        _seed_tenant_claim(
            db,
            tenant_id="tenant-b",
            claim_id="claim-b",
            claim_no="CLM-TENANT-B-001",
            amount="300.00",
        )
        db.add(
            BudgetAllocation(
                budget_no="BUD-LEGACY-DEFAULT-001",
                fiscal_year=now.year,
                period_type="year",
                period_key=str(now.year),
                department_name="财务部",
                subject_code="travel",
                subject_name="差旅费",
                original_amount=Decimal("10000.00"),
                adjusted_amount=Decimal("0.00"),
                status="active",
                warning_threshold=Decimal("80.00"),
                control_action="warn",
            )
        )
        db.commit()

        default_dashboard = FinanceDashboardService(db).build_dashboard()
        tenant_a_dashboard = FinanceDashboardService(
            db,
            tenant_id="tenant-a",
        ).build_dashboard()
        tenant_b_dashboard = FinanceDashboardService(
            db,
            tenant_id="tenant-b",
        ).build_dashboard()

        assert default_dashboard.totals["reimbursementCount"] == 1
        assert default_dashboard.totals["reimbursementAmount"] == 100.0
        assert default_dashboard.budget_summary["included"] is True
        assert default_dashboard.budget_summary["total"] == "¥10,000"

        assert tenant_a_dashboard.totals["reimbursementCount"] == 1
        assert tenant_a_dashboard.totals["reimbursementAmount"] == 200.0
        assert "CLM-TENANT-B-001" not in str(tenant_a_dashboard.top_claims)
        assert tenant_a_dashboard.budget_summary == {
            "ratio": 0.0,
            "total": "¥0",
            "used": "¥0",
            "left": "¥0",
            "included": False,
            "scope": "unavailable",
            "reason": "当前租户尚未接入独立预算池，预算指标未纳入统计。",
        }
        assert tenant_a_dashboard.totals["budgetUsageRate"] == 0.0
        assert all(
            metric["detail"] == "当前租户未接入独立预算池" and metric["tone"] == "neutral"
            for metric in tenant_a_dashboard.budget_metrics
        )
        assert "预算超支" not in {item["name"] for item in tenant_a_dashboard.bottlenecks}

        assert tenant_b_dashboard.totals["reimbursementCount"] == 1
        assert tenant_b_dashboard.totals["reimbursementAmount"] == 300.0
        assert tenant_b_dashboard.budget_summary["included"] is False


def test_finance_dashboard_uses_structured_claim_tenant_when_legacy_link_disagrees() -> None:
    session_factory = _session_factory()

    with session_factory() as db:
        claim = _claim(
            tenant_id="tenant-a",
            claim_id="structured-tenant-claim",
            claim_no="CLM-STRUCTURED-TENANT-001",
            amount="456.00",
        )
        db.add(claim)
        db.flush()
        # Case Link 是历史关联索引，不能覆盖 Claim 自身的结构化租户归属。
        _link_claim(db, claim, tenant_id="tenant-b")
        db.commit()

        tenant_a_dashboard = FinanceDashboardService(
            db,
            tenant_id="tenant-a",
        ).build_dashboard()
        tenant_b_dashboard = FinanceDashboardService(
            db,
            tenant_id="tenant-b",
        ).build_dashboard()

        assert tenant_a_dashboard.totals["reimbursementCount"] == 1
        assert tenant_a_dashboard.totals["reimbursementAmount"] == 456.0
        assert tenant_b_dashboard.totals["reimbursementCount"] == 0


def test_finance_dashboard_snapshot_cache_is_partitioned_by_tenant_and_data_scope() -> None:
    session_factory = _session_factory()

    with session_factory() as db:
        _seed_tenant_claim(
            db,
            tenant_id="tenant-a",
            claim_id="snapshot-claim-a",
            claim_no="CLM-SNAPSHOT-A-001",
            amount="880.00",
        )
        _seed_tenant_claim(
            db,
            tenant_id="tenant-b",
            claim_id="snapshot-claim-b",
            claim_no="CLM-SNAPSHOT-B-001",
            amount="990.00",
        )
        db.commit()

        tenant_a_service = FinanceDashboardSnapshotService(db, tenant_id="tenant-a")
        tenant_b_service = FinanceDashboardSnapshotService(db, tenant_id="tenant-b")
        first_a = tenant_a_service.build_dashboard()
        first_b = tenant_b_service.build_dashboard()
        second_a = tenant_a_service.build_dashboard()

        runs = list(
            db.scalars(
                select(AgentRun).where(
                    AgentRun.route_json["task_type"].as_string() == FINANCE_DASHBOARD_TASK_TYPE
                )
            ).all()
        )
        routes = [run.route_json or {} for run in runs]

        assert first_a.totals["reimbursementAmount"] == 880.0
        assert first_b.totals["reimbursementAmount"] == 990.0
        assert second_a.generated_at == first_a.generated_at
        assert len(runs) == 2
        assert {route["tenant_id"] for route in routes} == {"tenant-a", "tenant-b"}
        assert all(route["data_scope"] for route in routes)
        assert len({route["snapshot_key"] for route in routes}) == 2
        assert all(route["tenant_id"] in route["snapshot_key"] for route in routes)


def test_default_scheduler_snapshot_rejects_non_default_tenant_scope() -> None:
    session_factory = _session_factory()

    with session_factory() as db:
        service = FinanceDashboardSnapshotService(db, tenant_id="tenant-a")
        with pytest.raises(ValueError, match="default 系统租户"):
            service.refresh_default_snapshot()


@pytest.mark.parametrize("tenant_id", ["", "   "])
def test_finance_dashboard_access_policy_rejects_missing_tenant_scope(
    tenant_id: str,
) -> None:
    current_user = CurrentUserContext(
        username="finance-without-tenant",
        name="Finance Without Tenant",
        role_codes=["finance"],
        is_admin=False,
        tenant_id=tenant_id,
    )

    assert FinanceDashboardAccessPolicy.can_read(current_user) is False
    with pytest.raises(HTTPException) as exc_info:
        FinanceDashboardAccessPolicy.require_read(current_user)
    assert exc_info.value.status_code == 403


def test_finance_dashboard_endpoint_requires_finance_read_role_and_passes_tenant() -> None:
    session_factory = _session_factory()
    with session_factory() as db:
        _seed_tenant_claim(
            db,
            tenant_id="tenant-a",
            claim_id="endpoint-claim-a",
            claim_no="CLM-ENDPOINT-A-001",
            amount="1234.00",
        )
        db.commit()

    app = FastAPI()
    app.include_router(analytics_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
    client = TestClient(app)

    ordinary_response = client.get(
        "/analytics/finance-dashboard",
        headers={
            "X-Auth-Username": "ordinary",
            "X-Auth-Role-Codes": "user",
            "X-Auth-Tenant-Id": "tenant-a",
        },
    )
    budget_monitor_response = client.get(
        "/analytics/finance-dashboard",
        headers={
            "X-Auth-Username": "budget-monitor",
            "X-Auth-Role-Codes": "budget_monitor",
            "X-Auth-Tenant-Id": "tenant-a",
        },
    )
    finance_response = client.get(
        "/analytics/finance-dashboard",
        headers={
            "X-Auth-Username": "finance",
            "X-Auth-Role-Codes": "finance",
            "X-Auth-Tenant-Id": "tenant-a",
        },
    )
    executive_other_tenant_response = client.get(
        "/analytics/finance-dashboard",
        headers={
            "X-Auth-Username": "executive",
            "X-Auth-Role-Codes": "executive",
            "X-Auth-Tenant-Id": "tenant-b",
        },
    )
    admin_response = client.get(
        "/analytics/finance-dashboard",
        headers={
            "X-Auth-Username": "admin-reader",
            "X-Auth-Is-Admin": "true",
            "X-Auth-Tenant-Id": "tenant-b",
        },
    )

    assert ordinary_response.status_code == 403
    assert budget_monitor_response.status_code == 403
    assert finance_response.status_code == 200
    assert finance_response.json()["totals"]["reimbursementAmount"] == 1234.0
    assert executive_other_tenant_response.status_code == 200
    assert executive_other_tenant_response.json()["totals"]["reimbursementCount"] == 0
    assert admin_response.status_code == 200


def test_agent_run_endpoint_cannot_bypass_finance_snapshot_tenant_and_role_scope() -> None:
    session_factory = _session_factory()
    with session_factory() as db:
        _seed_finance_snapshot_run(
            db,
            run_id="run-finance-tenant-a",
            tenant_id="tenant-a",
            amount="111.00",
        )
        _seed_finance_snapshot_run(
            db,
            run_id="run-finance-tenant-b",
            tenant_id="tenant-b",
            amount="222.00",
        )
        _seed_finance_snapshot_run(
            db,
            run_id="run-finance-legacy-unscoped",
            tenant_id=None,
            amount="999.00",
        )
        _seed_finance_snapshot_run(
            db,
            run_id="run-finance-corrupt-scope",
            tenant_id="tenant-a",
            amount="777.00",
            route_data_scope="claims:corrupt;budget:corrupt",
        )
        db.commit()

    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
    client = TestClient(app)
    finance_a_headers = {
        "X-Auth-Username": "finance-a",
        "X-Auth-Role-Codes": "finance",
        "X-Auth-Tenant-Id": "tenant-a",
    }
    finance_b_headers = {
        "X-Auth-Username": "finance-b",
        "X-Auth-Role-Codes": "finance",
        "X-Auth-Tenant-Id": "tenant-b",
    }

    finance_a_list = client.get("/agent-runs", headers=finance_a_headers)
    ordinary_a_list = client.get(
        "/agent-runs",
        headers={
            "X-Auth-Username": "ordinary-a",
            "X-Auth-Role-Codes": "user",
            "X-Auth-Tenant-Id": "tenant-a",
        },
    )
    assert finance_a_list.status_code == 200
    assert [item["run_id"] for item in finance_a_list.json()] == ["run-finance-tenant-a"]
    assert ordinary_a_list.status_code == 200
    assert ordinary_a_list.json() == []

    allowed_detail = client.get(
        "/agent-runs/run-finance-tenant-a",
        headers=finance_a_headers,
    )
    assert allowed_detail.status_code == 200
    assert allowed_detail.json()["route_json"]["snapshot_payload"]["amount"] == "111.00"
    assert allowed_detail.json()["tool_calls"][0]["response_json"] == {"secret_amount": "111.00"}

    cross_tenant_detail = client.get(
        "/agent-runs/run-finance-tenant-a",
        headers=finance_b_headers,
    )
    cross_tenant_admin_detail = client.get(
        "/agent-runs/run-finance-tenant-a",
        headers={
            "X-Auth-Username": "admin-b",
            "X-Auth-Is-Admin": "true",
            "X-Auth-Tenant-Id": "tenant-b",
        },
    )
    same_tenant_ordinary_detail = client.get(
        "/agent-runs/run-finance-tenant-a",
        headers={
            "X-Auth-Username": "ordinary-a",
            "X-Auth-Role-Codes": "user",
            "X-Auth-Tenant-Id": "tenant-a",
        },
    )
    legacy_unscoped_detail = client.get(
        "/agent-runs/run-finance-legacy-unscoped",
        headers={
            "X-Auth-Username": "finance-default",
            "X-Auth-Role-Codes": "finance",
            "X-Auth-Tenant-Id": "default",
        },
    )
    corrupt_scope_detail = client.get(
        "/agent-runs/run-finance-corrupt-scope",
        headers=finance_a_headers,
    )

    assert cross_tenant_detail.status_code == 404
    assert cross_tenant_admin_detail.status_code == 404
    assert same_tenant_ordinary_detail.status_code == 403
    assert legacy_unscoped_detail.status_code == 404
    assert corrupt_scope_detail.status_code == 404
