from __future__ import annotations

import argparse
import importlib.util
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from pathlib import Path

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

import app.models  # noqa: F401 - 注册完整 metadata
from app.cli.savings_standard_adjustment_backfill import (
    StandardAdjustmentBackfillDisposition,
    StandardAdjustmentSavingsBackfillService,
)
from app.db.base_class import Base
from app.models.expense_case import BusinessEvent, ExpenseCase, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.savings import (
    ProfileBaselineSnapshot,
    SavingsEvent,
    SavingsEvidenceLink,
    SavingsOpportunity,
)
from app.services.expense_claim_standard_adjustment import ExpenseClaimStandardAdjustmentMixin


@pytest.fixture()
def db() -> Session:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    with factory() as session:
        yield session
    Base.metadata.drop_all(engine)
    engine.dispose()


def test_dry_run_reports_eligible_without_any_write(db: Session) -> None:
    _seed_adjusted_claim(db, tenant_id="tenant-a")
    db.commit()
    before = _write_counts(db)

    preview = StandardAdjustmentSavingsBackfillService(
        db,
        tenant_id="tenant-a",
    ).preview()

    assert preview.claims_inspected == 1
    assert preview.flags_inspected == 1
    assert preview.eligible == 1
    assert preview.replayed == 0
    assert preview.skipped == 0
    assert preview.reasons == {"eligible": 1}
    assert _write_counts(db) == before
    assert not db.new
    assert not db.dirty


def test_apply_creates_once_and_second_apply_is_replayed(db: Session) -> None:
    claim, item = _seed_adjusted_claim(db, tenant_id="tenant-a")
    db.commit()
    service = StandardAdjustmentSavingsBackfillService(db, tenant_id="tenant-a")

    applied = service.apply_batch(run_id="run-001")
    db.commit()

    assert applied.created == 1
    assert applied.replayed == 0
    assert applied.skipped == 0
    opportunity = db.scalar(
        select(SavingsOpportunity).where(
            SavingsOpportunity.tenant_id == "tenant-a",
            SavingsOpportunity.claim_id == claim.id,
        )
    )
    assert opportunity is not None
    assert opportunity.claim_item_id == item.id
    assert opportunity.estimated_net == Decimal("400.0000")
    assert opportunity.status == "in_progress"
    assert db.scalar(
        select(func.count(SavingsEvidenceLink.id)).where(
            SavingsEvidenceLink.tenant_id == "tenant-a"
        )
    ) == 2
    counts_after_first = _write_counts(db)

    replayed = service.apply_batch(run_id="run-002")
    db.commit()

    assert replayed.created == 0
    assert replayed.replayed == 1
    assert replayed.skipped == 0
    assert replayed.items[0].opportunity_id == opportunity.id
    assert _write_counts(db) == counts_after_first


def test_apply_skips_cross_tenant_missing_case_and_missing_evidence(db: Session) -> None:
    _seed_adjusted_claim(db, tenant_id="tenant-b")
    _seed_adjusted_claim(db, tenant_id=None)
    _seed_adjusted_claim(
        db,
        tenant_id="tenant-a",
        flag_overrides={"policy_rule_version": "", "calculation_fingerprint": ""},
    )
    db.commit()
    service = StandardAdjustmentSavingsBackfillService(db, tenant_id="tenant-a")

    preview = service.preview()

    dispositions = {item.disposition for item in preview.items}
    assert StandardAdjustmentBackfillDisposition.TENANT_CONFLICT in dispositions
    assert StandardAdjustmentBackfillDisposition.MISSING_CASE_LINK in dispositions
    assert StandardAdjustmentBackfillDisposition.MISSING_POLICY_VERSION in dispositions
    assert preview.eligible == 0
    assert preview.skipped == 3

    applied = service.apply_batch(run_id="run-quality-report")
    db.commit()

    assert applied.created == 0
    assert applied.replayed == 0
    assert applied.skipped == 3
    assert db.scalar(select(func.count(SavingsOpportunity.id))) == 0
    assert db.scalar(select(func.count(ProfileBaselineSnapshot.id))) == 0


def test_tampered_amount_or_policy_snapshot_is_never_monetized(db: Session) -> None:
    _seed_adjusted_claim(
        db,
        tenant_id="tenant-a",
        flag_overrides={"original_amount": "9999.00"},
    )
    _seed_adjusted_claim(
        db,
        tenant_id="tenant-a",
        flag_overrides={"policy_hotel_rate": "451.00"},
    )
    db.commit()

    preview = StandardAdjustmentSavingsBackfillService(
        db,
        tenant_id="tenant-a",
    ).preview()

    dispositions = [item.disposition for item in preview.items]
    assert StandardAdjustmentBackfillDisposition.ORIGINAL_AMOUNT_MISMATCH in dispositions
    assert (
        StandardAdjustmentBackfillDisposition.CALCULATION_FINGERPRINT_MISMATCH
        in dispositions
    )
    assert preview.eligible == 0


def test_cli_defaults_to_dry_run_and_apply_requires_target_confirmation(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    module = _load_cli()
    args = module.build_parser().parse_args(
        [
            "--tenant-id",
            "tenant-a",
            "--created-before",
            "2026-07-16T00:00:00+08:00",
            "--expected-host",
            "migration-probe",
            "--expected-database",
            "migration_probe",
        ]
    )
    assert args.apply is False
    assert args.created_before == datetime(2026, 7, 15, 16, 0, tzinfo=UTC)

    apply_args = module.build_parser().parse_args(
        [
            "--apply",
            "--tenant-id",
            "tenant-a",
            "--created-before",
            "2026-07-16T00:00:00Z",
            "--expected-host",
            "migration-probe",
            "--expected-database",
            "migration_probe",
        ]
    )
    monkeypatch.delenv("DATABASE_URL", raising=False)
    with pytest.raises(module.BackfillCommandError) as exc_info:
        module.run(apply_args)
    assert exc_info.value.code == "confirm_target_required"


@pytest.mark.parametrize(
    ("current_revision", "expected"),
    [
        ("20260716_0015", True),
        ("20260716_0016", True),
        ("20260716_0017", True),
        ("20260716_0014", False),
        (None, False),
    ],
)
def test_cli_accepts_any_migration_descended_from_required_savings_revision(
    current_revision: str | None,
    expected: bool,
) -> None:
    module = _load_cli()

    assert module.revision_contains_required(current_revision) is expected


@pytest.mark.parametrize("value", ["", "2026-07-16T00:00:00", "not-a-time"])
def test_cli_rejects_timestamp_without_explicit_timezone(value: str) -> None:
    with pytest.raises(argparse.ArgumentTypeError):
        _load_cli().parse_timestamp(value)


def _seed_adjusted_claim(
    db: Session,
    *,
    tenant_id: str | None,
    flag_overrides: dict[str, object] | None = None,
) -> tuple[ExpenseClaim, ExpenseClaimItem]:
    claim_id = str(uuid.uuid4())
    item_id = str(uuid.uuid4())
    suffix = uuid.uuid4().hex[:10]
    policy_result = {
        "days": 2,
        "location": "上海市",
        "matched_city": "上海",
        "grade": "P6",
        "grade_band": "P6-P7",
        "hotel_rate": Decimal("400.00"),
        "hotel_amount": Decimal("800.00"),
        "rule_name": "差旅住宿标准",
        "rule_version": "hotel-policy-v3",
    }
    flag: dict[str, object] = {
        "source": "reimbursement_standard_adjustment",
        "event_type": "standard_adjustment_accepted",
        "calculation_source": "server_policy",
        "item_id": item_id,
        "original_amount": "1200.00",
        "reimbursable_amount": "800.00",
        "employee_absorbed_amount": "400.00",
        "policy_days": policy_result["days"],
        "policy_location": policy_result["location"],
        "policy_matched_city": policy_result["matched_city"],
        "policy_grade": policy_result["grade"],
        "policy_grade_band": policy_result["grade_band"],
        "policy_hotel_rate": "400.00",
        "policy_hotel_amount": "800.00",
        "policy_rule_name": policy_result["rule_name"],
        "policy_rule_version": policy_result["rule_version"],
        "policy_rule_version_source": "finance_rules_content_hash",
    }
    flag["calculation_fingerprint"] = (
        ExpenseClaimStandardAdjustmentMixin._standard_adjustment_calculation_fingerprint(
            item_id=item_id,
            original_amount=Decimal("1200.00"),
            reimbursable_amount=Decimal("800.00"),
            policy_result=policy_result,
        )
    )
    flag.update(flag_overrides or {})
    claim = ExpenseClaim(
        id=claim_id,
        claim_no=f"BX-BACKFILL-{suffix}",
        employee_name="历史员工",
        department_name="销售部",
        project_code="PRJ-BACKFILL",
        expense_type="hotel",
        reason="历史住宿报销",
        location="上海",
        amount=Decimal("800.00"),
        currency="CNY",
        invoice_count=1,
        occurred_at=datetime(2026, 6, 1, 9, 0, tzinfo=UTC),
        submitted_at=datetime(2026, 6, 2, 9, 0, tzinfo=UTC),
        status="submitted",
        approval_stage="财务审批",
        risk_flags_json=[flag],
        created_at=datetime(2026, 6, 1, 8, 0, tzinfo=UTC),
        updated_at=datetime(2026, 6, 2, 8, 0, tzinfo=UTC),
    )
    item = ExpenseClaimItem(
        id=item_id,
        claim=claim,
        item_date=date(2026, 6, 1),
        item_type="hotel",
        item_reason="上海住宿 2 晚",
        item_location="上海",
        item_note="",
        item_amount=Decimal("1200.00"),
    )
    db.add(claim)
    if tenant_id is not None:
        expense_case = ExpenseCase(
            id=str(uuid.uuid4()),
            tenant_id=tenant_id,
            case_no=f"CASE-{suffix}",
            scene_code="travel",
            title="历史住宿费用事件",
            current_stage="claiming",
            status="active",
        )
        db.add_all(
            [
                expense_case,
                ExpenseCaseLink(
                    id=str(uuid.uuid4()),
                    tenant_id=tenant_id,
                    expense_case_id=expense_case.id,
                    resource_type="expense_claim",
                    resource_id=claim.id,
                    relation_type="reimbursement",
                ),
            ]
        )
    return claim, item


def _write_counts(db: Session) -> tuple[int, ...]:
    return tuple(
        int(db.scalar(select(func.count()).select_from(model)) or 0)
        for model in (
            SavingsOpportunity,
            ProfileBaselineSnapshot,
            SavingsEvidenceLink,
            SavingsEvent,
            BusinessEvent,
        )
    )


def _load_cli():
    path = (
        Path(__file__).resolve().parents[1]
        / "scripts"
        / "backfill_standard_adjustment_savings.py"
    )
    spec = importlib.util.spec_from_file_location("backfill_standard_adjustment_savings_cli", path)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module
