from __future__ import annotations

from datetime import UTC, datetime, timedelta
from decimal import Decimal
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from typing import Any

import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.core.config import get_settings
from app.db.base import Base
from app.models.agent_run import AgentRun, AgentToolCall
from app.models.financial_record import ExpenseClaim
from app.models.tenant import Tenant
from app.models.tenant_finance_report import TenantFinanceReportRun
from app.services.digital_employee_dashboard import DigitalEmployeeDashboardService
from app.services.digital_employee_finance_report_task import (
    DigitalEmployeeFinanceReportTaskService,
)
from app.services.finance_report_tenant import TenantFinanceReportConfigService
from app.services.hermes_risk_clue_collector import HermesRiskClueCollectorService
from app.services.hermes_risk_scanner import HermesRiskScannerService


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 _claim(
    *,
    tenant_id: str,
    claim_id: str,
    claim_no: str,
    amount: str,
    now: datetime,
) -> ExpenseClaim:
    return ExpenseClaim(
        id=claim_id,
        tenant_id=tenant_id,
        claim_no=claim_no,
        employee_name=f"{tenant_id} 员工",
        department_name=f"{tenant_id} 财务部",
        expense_type="travel",
        reason="客户拜访",
        location="上海",
        amount=Decimal(amount),
        invoice_count=1,
        occurred_at=now - timedelta(days=1),
        submitted_at=now - timedelta(days=1),
        status="submitted",
        approval_stage="直属领导审批",
        risk_flags_json=[],
        hermes_risk_flag=False,
        created_at=now - timedelta(days=1),
        updated_at=now,
    )


def _digital_run(
    *,
    tenant_id: str,
    run_id: str,
    scanned_claim_count: int,
    now: datetime,
) -> AgentRun:
    return AgentRun(
        run_id=run_id,
        agent="hermes",
        source="schedule",
        user_id="digital_employee",
        status="succeeded",
        route_json={
            "tenant_id": tenant_id,
            "task_type": "global_risk_scan",
        },
        ontology_json={"tenant_id": tenant_id},
        result_summary="风险扫描完成。",
        started_at=now - timedelta(minutes=2),
        finished_at=now - timedelta(minutes=1),
        tool_calls=[
            AgentToolCall(
                run_id=run_id,
                tool_type="rule_engine",
                tool_name="digital_employee.financial_risk_graph.scan",
                request_json={"tenant_id": tenant_id},
                response_json={"scanned_claim_count": scanned_claim_count},
                status="succeeded",
                duration_ms=10,
                created_at=now - timedelta(minutes=2),
            )
        ],
    )


def test_hermes_scanners_and_dashboard_do_not_mix_tenants() -> None:
    factory = _session_factory()
    now = datetime.now(UTC)
    with factory() as db:
        db.add_all([_tenant("tenant-a"), _tenant("tenant-b")])
        db.add_all(
            [
                _claim(
                    tenant_id="tenant-a",
                    claim_id="claim-a",
                    claim_no="RE-A-001",
                    amount="100.00",
                    now=now,
                ),
                _claim(
                    tenant_id="tenant-b",
                    claim_id="claim-b",
                    claim_no="RE-B-SECRET",
                    amount="9000.00",
                    now=now,
                ),
                _digital_run(
                    tenant_id="tenant-a",
                    run_id="run-a",
                    scanned_claim_count=1,
                    now=now,
                ),
                _digital_run(
                    tenant_id="tenant-b",
                    run_id="run-b",
                    scanned_claim_count=99,
                    now=now,
                ),
            ]
        )
        db.commit()

        fetched = HermesRiskScannerService(db)._fetch_unscanned_claims(tenant_id="tenant-a")
        assert [row.id for row in fetched] == ["claim-a"]

        clues = HermesRiskClueCollectorService(db).collect_risk_clues(tenant_id="tenant-a")
        assert clues["tenant_id"] == "tenant-a"
        assert [row["claim_no"] for row in clues["facts"]] == ["RE-A-001"]

        dashboard = DigitalEmployeeDashboardService(
            db,
            tenant_id="tenant-a",
        ).build_dashboard(days=7)
        assert dashboard.totals["totalRuns"] == 1
        assert dashboard.totals["riskObservations"] == 0
        assert [row["runId"] for row in dashboard.recent_runs] == ["run-a"]


def test_finance_report_recipients_data_path_and_idempotency_are_tenant_bound(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path))
    get_settings.cache_clear()
    factory = _session_factory()
    now = datetime.now(UTC)
    start_date = (now - timedelta(days=3)).date()
    end_date = now.date()
    try:
        with factory() as db:
            db.add_all([_tenant("tenant-a"), _tenant("tenant-b")])
            db.add_all(
                [
                    _claim(
                        tenant_id="tenant-a",
                        claim_id="claim-a",
                        claim_no="RE-A-001",
                        amount="100.00",
                        now=now,
                    ),
                    _claim(
                        tenant_id="tenant-b",
                        claim_id="claim-b",
                        claim_no="RE-B-001",
                        amount="9000.00",
                        now=now,
                    ),
                ]
            )
            db.commit()

            config_service = TenantFinanceReportConfigService(db)
            config_service.upsert(
                tenant_id="tenant-a",
                recipients=["finance-a@example.com"],
                delivery_enabled=True,
                updated_by="finance-a",
            )
            config_service.upsert(
                tenant_id="tenant-b",
                recipients=["finance-b@example.com"],
                delivery_enabled=True,
                updated_by="finance-b",
            )
            assert config_service.configured_recipients(tenant_id="tenant-a") == [
                "finance-a@example.com"
            ]
            assert (
                config_service.configured_recipients(
                    tenant_id="tenant-a",
                    requested=["finance-b@example.com"],
                )
                == []
            )
            assert config_service.configured_recipients(tenant_id="missing") == []

            task = DigitalEmployeeFinanceReportTaskService(db)
            report_a = task.generate_report(
                report_type="weekly",
                start_date=start_date,
                end_date=end_date,
                tenant_id="tenant-a",
                send_email=False,
            )
            replay_a = task.generate_report(
                report_type="weekly",
                start_date=start_date,
                end_date=end_date,
                tenant_id="tenant-a",
                send_email=False,
            )
            report_b = task.generate_report(
                report_type="weekly",
                start_date=start_date,
                end_date=end_date,
                tenant_id="tenant-b",
                send_email=False,
            )

            assert report_a["summary"]["reimbursement_count"] == 1
            assert report_a["summary"]["reimbursement_amount"] == 100.0
            assert report_b["summary"]["reimbursement_count"] == 1
            assert report_b["summary"]["reimbursement_amount"] == 9000.0
            assert replay_a["idempotent_replay"] is True
            assert report_a["pdf"]["storage_key"] != report_b["pdf"]["storage_key"]
            assert "/tenants/" in report_a["pdf"]["storage_key"]
            assert "tenant-a" not in report_a["pdf"]["storage_key"]

            ledger_rows = list(db.scalars(select(TenantFinanceReportRun)).all())
            assert len(ledger_rows) == 2
            assert {row.tenant_id for row in ledger_rows} == {"tenant-a", "tenant-b"}
            assert len({row.idempotency_key for row in ledger_rows}) == 2

            runs = list(db.scalars(select(AgentRun).where(AgentRun.agent == "hermes")))
            tenant_a_runs = [
                row for row in runs if (row.route_json or {}).get("tenant_id") == "tenant-a"
            ]
            assert len(tenant_a_runs) == 1
            assert tenant_a_runs[0].ontology_json["tenant_id"] == "tenant-a"
    finally:
        get_settings.cache_clear()


class _UnsupportedDialectOperationGuard:
    def __init__(self) -> None:
        self.bind = type("Bind", (), {"dialect": type("Dialect", (), {"name": "sqlite"})()})()
        self.mutation_calls: list[str] = []

    def get_bind(self) -> Any:
        return self.bind

    def __getattr__(self, name: str) -> Any:
        self.mutation_calls.append(name)
        raise AssertionError(f"unsupported dialect attempted migration operation: {name}")


@pytest.mark.parametrize("direction", ["upgrade", "downgrade"])
def test_hermes_tenant_migration_rejects_unsupported_dialect_before_mutation(
    direction: str,
) -> None:
    migration_path = (
        Path(__file__).resolve().parents[1]
        / "alembic"
        / "versions"
        / "20260717_0028_hermes_ontology_tenant_security.py"
    )
    spec = spec_from_file_location("migration_20260717_0028_test", migration_path)
    assert spec is not None and spec.loader is not None
    module = module_from_spec(spec)
    spec.loader.exec_module(module)
    guard = _UnsupportedDialectOperationGuard()
    module.op = guard

    with pytest.raises(RuntimeError, match="only supports PostgreSQL"):
        getattr(module, direction)()

    assert guard.mutation_calls == []
