from __future__ import annotations

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

import pytest
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_current_user, get_db
from app.db.base import Base
from app.main import create_app
from app.models.agent_run import AgentRun
from app.models.employee import Employee
from app.models.financial_record import (
    AccountsPayableRecord,
    AccountsReceivableRecord,
    ExpenseClaim,
)
from app.models.organization import OrganizationUnit
from app.models.tenant import Tenant
from app.schemas.ontology import OntologyParseRequest
from app.schemas.orchestrator import OrchestratorRequest
from app.services.ontology import SemanticOntologyService
from app.services.orchestrator import OrchestratorService


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 _tenant(tenant_id: str) -> Tenant:
    return Tenant(
        tenant_id=tenant_id,
        tenant_code=tenant_id,
        name=f"{tenant_id} 公司",
        status="active",
    )


def _employee(
    *,
    tenant_id: str,
    employee_id: str,
    employee_no: str,
    name: str,
    email: str,
    manager_id: str | None = None,
) -> Employee:
    return Employee(
        id=employee_id,
        tenant_id=tenant_id,
        employee_no=employee_no,
        name=name,
        email=email,
        manager_id=manager_id,
    )


def _claim(
    *,
    tenant_id: str,
    claim_id: str,
    claim_no: str,
    employee_id: str,
    employee_name: str,
    department_id: str,
    department_name: str,
    project_code: str,
) -> ExpenseClaim:
    now = datetime.now(UTC)
    return ExpenseClaim(
        id=claim_id,
        tenant_id=tenant_id,
        claim_no=claim_no,
        employee_id=employee_id,
        employee_name=employee_name,
        department_id=department_id,
        department_name=department_name,
        project_code=project_code,
        expense_type="travel",
        reason="客户拜访",
        location="上海",
        amount=Decimal("100.00"),
        invoice_count=1,
        occurred_at=now,
        submitted_at=now,
        status="submitted",
        approval_stage="直属领导审批",
        risk_flags_json=[],
    )


def test_ontology_reference_catalog_never_reads_another_tenant() -> None:
    factory = _session_factory()
    today = datetime.now(UTC).date()
    with factory() as db:
        db.add_all([_tenant("tenant-a"), _tenant("tenant-b")])
        db.add_all(
            [
                OrganizationUnit(
                    id="dept-a",
                    tenant_id="tenant-a",
                    unit_code="A-FIN",
                    name="甲方财务部",
                ),
                OrganizationUnit(
                    id="dept-b",
                    tenant_id="tenant-b",
                    unit_code="B-FIN",
                    name="乙方机密部门",
                ),
                _employee(
                    tenant_id="tenant-a",
                    employee_id="employee-a",
                    employee_no="A001",
                    name="甲方员工",
                    email="employee-a@example.com",
                ),
                _employee(
                    tenant_id="tenant-b",
                    employee_id="employee-b",
                    employee_no="B001",
                    name="乙方机密员工",
                    email="employee-b@example.com",
                ),
            ]
        )
        db.add_all(
            [
                _claim(
                    tenant_id="tenant-a",
                    claim_id="claim-a",
                    claim_no="RE-A-001",
                    employee_id="employee-a",
                    employee_name="甲方员工",
                    department_id="dept-a",
                    department_name="甲方财务部",
                    project_code="PROJECT-A",
                ),
                _claim(
                    tenant_id="tenant-b",
                    claim_id="claim-b",
                    claim_no="RE-B-001",
                    employee_id="employee-b",
                    employee_name="乙方机密员工",
                    department_id="dept-b",
                    department_name="乙方机密部门",
                    project_code="PROJECT-B-SECRET",
                ),
                AccountsReceivableRecord(
                    tenant_id="tenant-a",
                    receivable_no="AR-A-001",
                    customer_id="customer-a",
                    customer_name="甲方客户",
                    amount_receivable=Decimal("100"),
                    amount_received=Decimal("0"),
                    amount_outstanding=Decimal("100"),
                    posting_date=today,
                    due_date=today,
                    status="open",
                ),
                AccountsReceivableRecord(
                    tenant_id="tenant-b",
                    receivable_no="AR-B-001",
                    customer_id="customer-b",
                    customer_name="乙方机密客户",
                    amount_receivable=Decimal("200"),
                    amount_received=Decimal("0"),
                    amount_outstanding=Decimal("200"),
                    posting_date=today,
                    due_date=today,
                    status="open",
                ),
                AccountsPayableRecord(
                    tenant_id="tenant-a",
                    payable_no="AP-A-001",
                    vendor_id="vendor-a",
                    vendor_name="甲方供应商",
                    amount_payable=Decimal("100"),
                    amount_paid=Decimal("0"),
                    amount_outstanding=Decimal("100"),
                    posting_date=today,
                    due_date=today,
                    status="open",
                ),
                AccountsPayableRecord(
                    tenant_id="tenant-b",
                    payable_no="AP-B-001",
                    vendor_id="vendor-b",
                    vendor_name="乙方机密供应商",
                    amount_payable=Decimal("200"),
                    amount_paid=Decimal("0"),
                    amount_outstanding=Decimal("200"),
                    posting_date=today,
                    due_date=today,
                    status="open",
                ),
            ]
        )
        db.commit()

        catalog = SemanticOntologyService(db)._load_reference_catalog(tenant_id="tenant-a")

        assert catalog.employees == ["甲方员工"]
        assert catalog.departments == ["甲方财务部"]
        assert catalog.customers == ["甲方客户"]
        assert catalog.vendors == ["甲方供应商"]
        assert catalog.projects == ["PROJECT-A"]
        all_values = [
            *catalog.employees,
            *catalog.departments,
            *catalog.customers,
            *catalog.vendors,
            *catalog.projects,
        ]
        assert all("机密" not in value for value in all_values)


def test_ontology_creates_tenant_run_before_model_and_persists_failure(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    factory = _session_factory()
    with factory() as db:
        db.add(_tenant("tenant-a"))
        db.commit()
        service = SemanticOntologyService(db)
        observed: dict[str, str] = {}

        def fake_model_parse(**kwargs):
            context = kwargs["operation_context"]
            run = db.scalar(select(AgentRun).where(AgentRun.run_id == context.run_id))
            assert run is not None
            assert run.status == "running"
            assert run.route_json["tenant_id"] == "tenant-a"
            assert run.route_json["phase"] == "pending_model_analysis"
            assert context.tenant_id == "tenant-a"
            observed["run_id"] = context.run_id
            return None, [], None

        monkeypatch.setattr(service, "_parse_with_model", fake_model_parse)
        result = service.parse(
            OntologyParseRequest(query="查询本月报销金额", user_id="employee-a"),
            tenant_id="tenant-a",
        )

        assert result.run_id == observed["run_id"]
        succeeded = db.scalar(select(AgentRun).where(AgentRun.run_id == result.run_id))
        assert succeeded is not None
        assert succeeded.route_json["tenant_id"] == "tenant-a"
        assert succeeded.ontology_json["tenant_id"] == "tenant-a"

        with pytest.raises(ValueError, match="仅支持财务业务"):
            service.parse(
                OntologyParseRequest(query="今天天气怎么样", user_id="employee-a"),
                tenant_id="tenant-a",
            )
        failed = db.scalars(
            select(AgentRun).where(AgentRun.status == "failed").order_by(AgentRun.started_at.desc())
        ).first()
        assert failed is not None
        assert failed.route_json["tenant_id"] == "tenant-a"
        assert failed.route_json["phase"] == "failed"


def test_employee_profile_api_hides_cross_tenant_and_enforces_manager_scope() -> None:
    factory = _session_factory()
    with factory() as db:
        db.add_all([_tenant("tenant-a"), _tenant("tenant-b")])
        db.add_all(
            [
                _employee(
                    tenant_id="tenant-a",
                    employee_id="manager-a",
                    employee_no="A-MGR",
                    name="甲方经理",
                    email="manager-a@example.com",
                ),
                _employee(
                    tenant_id="tenant-a",
                    employee_id="employee-a",
                    employee_no="A001",
                    name="甲方员工",
                    email="employee-a@example.com",
                    manager_id="manager-a",
                ),
                _employee(
                    tenant_id="tenant-b",
                    employee_id="employee-b",
                    employee_no="B001",
                    name="乙方员工",
                    email="employee-b@example.com",
                ),
            ]
        )
        db.add(
            _claim(
                tenant_id="tenant-b",
                claim_id="claim-b",
                claim_no="RE-B-001",
                employee_id="employee-b",
                employee_name="乙方员工",
                department_id="dept-b",
                department_name="乙方部门",
                project_code="PROJECT-B",
            )
        )
        db.commit()

    current = {
        "user": CurrentUserContext(
            username="manager-a@example.com",
            name="甲方经理",
            role_codes=["manager"],
            is_admin=False,
            tenant_id="tenant-a",
            employee_id="manager-a",
        )
    }
    app = create_app()

    def override_db() -> Generator[Session, None, None]:
        db = factory()
        try:
            yield db
        finally:
            db.close()

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[get_current_user] = lambda: current["user"]
    client = TestClient(app)

    assert client.get("/api/v1/employee-profiles/employee-a/latest").status_code == 200
    assert client.get("/api/v1/employee-profiles/employee-b/latest").status_code == 404
    assert (
        client.get(
            "/api/v1/employee-profiles/employee-a/latest",
            params={"claim_id": "claim-b"},
        ).status_code
        == 404
    )

    current["user"] = CurrentUserContext(
        username="employee-a@example.com",
        name="甲方员工",
        role_codes=[],
        is_admin=False,
        tenant_id="tenant-a",
        employee_id="employee-a",
    )
    assert client.get("/api/v1/employee-profiles/manager-a/latest").status_code == 404
    assert client.get("/api/v1/employee-profiles/employee-a/latest").status_code == 200


def test_orchestrator_rejects_untrusted_or_mismatched_tenant_before_run() -> None:
    factory = _session_factory()
    payload = OrchestratorRequest(
        source="user_message",
        user_id="employee-a@example.com",
        message="查询本月报销金额",
        context_json={"tenant_id": "forged-tenant"},
    )
    with factory() as db:
        db.add(_tenant("tenant-a"))
        db.add(
            Tenant(
                tenant_id="tenant-suspended",
                tenant_code="tenant-suspended",
                name="已停用公司",
                status="suspended",
            )
        )
        db.commit()
        service = OrchestratorService(db)

        with pytest.raises(ValueError, match="缺少可信租户上下文"):
            service.run(payload)
        with pytest.raises(ValueError, match="不存在或未启用"):
            service.run(payload, trusted_tenant_id="unknown-tenant")
        with pytest.raises(ValueError, match="不存在或未启用"):
            service.run(payload, trusted_tenant_id="tenant-suspended")

        current_user = CurrentUserContext(
            username="employee-a@example.com",
            name="甲方员工",
            role_codes=[],
            is_admin=False,
            tenant_id="tenant-a",
            employee_id="employee-a",
        )
        with pytest.raises(ValueError, match="租户不一致"):
            service.run(
                payload,
                current_user=current_user,
                trusted_tenant_id="tenant-b",
            )

        assert db.scalar(select(AgentRun.id)) is None
