"""费用审核智能体模块测试 — 规则CRUD + 报销单自动校验 + 审批流 + 看板统计

覆盖 expenses.py 全部13个端点：
rules(CRUD+seed) / reimbursements(submit/list/detail/approve/reject/return/resubmit) / stats
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from tests.conftest import create_test_user, get_token_for_user, auth_header
from app.models import ExpenseRule, ExpenseReimbursement, User
import hashlib


def mk_rule(db: Session, **kwargs) -> ExpenseRule:
    defaults = {
        "rule_name": "测试规则", "dimension": "expense_type", "dimension_value": "",
        "expense_type": "entertainment", "limit_type": "single", "limit_amount": 2000.0,
        "cycle": "single", "status": "active", "remark": "",
    }
    defaults.update(kwargs)
    r = ExpenseRule(**defaults)
    db.add(r)
    db.commit()
    db.refresh(r)
    return r


def mk_reimb(db: Session, **kwargs) -> ExpenseReimbursement:
    defaults = {
        "reimb_no": "BX_TEST001", "applicant": "张三", "department": "销售部",
        "expense_type": "entertainment", "title": "客户招待", "amount": 500.0,
        "status": "pending", "check_result": "pass", "created_by": "test",
    }
    defaults.update(kwargs)
    r = ExpenseReimbursement(**defaults)
    db.add(r)
    db.commit()
    db.refresh(r)
    return r


class TestRules:
    """费用规则 CRUD"""

    BASE = "/api/cma/expenses/rules"

    def test_list_empty(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.get(self.BASE, headers=auth_header(token))
        assert resp.status_code == 200
        assert resp.json()["data"] == []
        assert resp.json()["total"] == 0

    def test_list_filters(self, client: TestClient, db: Session):
        """expense_type / status 过滤"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待限额", expense_type="entertainment")
        mk_rule(db, rule_name="差旅限额", expense_type="travel")
        mk_rule(db, rule_name="停用规则", expense_type="office", status="inactive")

        r = client.get(f"{self.BASE}?expense_type=travel", headers=auth_header(token))
        assert [x["rule_name"] for x in r.json()["data"]] == ["差旅限额"]
        r = client.get(f"{self.BASE}?status=inactive", headers=auth_header(token))
        assert [x["rule_name"] for x in r.json()["data"]] == ["停用规则"]
        # 标签映射
        r = client.get(f"{self.BASE}?expense_type=entertainment", headers=auth_header(token))
        assert r.json()["data"][0]["expense_type_label"] == "招待费"

    def test_create_rule(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"rule_name": "招待费单笔限额", "expense_type": "entertainment",
                                 "limit_type": "single", "limit_amount": 2000.0})
        assert resp.status_code == 200
        assert resp.json()["message"] == "规则已创建"
        assert resp.json()["id"] > 0

    def test_create_rule_missing_params(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token), json={"rule_name": "缺参数"})
        assert resp.status_code == 400
        assert "缺少必要参数" in resp.json()["detail"]

    def test_create_rule_invalid_expense_type(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"rule_name": "非法类型", "expense_type": "gambling",
                                 "limit_amount": 100})
        assert resp.status_code == 400
        assert "无效费用类型" in resp.json()["detail"]

    def test_create_rule_invalid_limit_type(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"rule_name": "非法限额", "expense_type": "office",
                                 "limit_type": "per_month", "limit_amount": 100})
        assert resp.status_code == 400
        assert "无效限额类型" in resp.json()["detail"]

    def test_update_rule(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        rule = mk_rule(db)
        resp = client.put(f"{self.BASE}/{rule.id}", headers=auth_header(token),
                          json={"rule_name": "新规则名", "limit_amount": 5000.0, "status": "inactive"})
        assert resp.status_code == 200
        assert resp.json()["message"] == "规则已更新"
        r = client.get(self.BASE, headers=auth_header(token))
        updated = r.json()["data"][0]
        assert updated["rule_name"] == "新规则名"
        assert updated["limit_amount"] == 5000.0
        assert updated["status"] == "inactive"

    def test_update_rule_not_found(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.put(f"{self.BASE}/99999", headers=auth_header(token), json={"rule_name": "x"})
        assert resp.status_code == 404

    def test_update_rule_invalid_type(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        rule = mk_rule(db)
        resp = client.put(f"{self.BASE}/{rule.id}", headers=auth_header(token),
                          json={"expense_type": "bogus"})
        assert resp.status_code == 400

    def test_delete_rule(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        rule = mk_rule(db)
        resp = client.delete(f"{self.BASE}/{rule.id}", headers=auth_header(token))
        assert resp.status_code == 200
        assert resp.json()["message"] == "规则已删除"
        r = client.get(self.BASE, headers=auth_header(token))
        assert r.json()["data"] == []

    def test_delete_rule_not_found(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.delete(f"{self.BASE}/99999", headers=auth_header(token))
        assert resp.status_code == 404

    def test_seed_rules_idempotent(self, client: TestClient, db: Session):
        """预置规则幂等：首次7条，再次0条"""
        create_test_user(db)
        token = get_token_for_user(client)
        r1 = client.post(f"{self.BASE}/seed", headers=auth_header(token))
        assert r1.status_code == 200
        assert "新增 7 条" in r1.json()["message"]
        r2 = client.post(f"{self.BASE}/seed", headers=auth_header(token))
        assert "新增 0 条" in r2.json()["message"]
        r = client.get(self.BASE, headers=auth_header(token))
        assert r.json()["total"] == 7

    def test_rule_permission_business_denied(self, client: TestClient, db: Session):
        """business角色不能创建规则 → 403"""
        u = User(username="biz_expense", password_hash=hashlib.sha256("pass123".encode()).hexdigest(),
                 name="业务员", role="business")
        db.add(u)
        db.commit()
        token = get_token_for_user(client, username="biz_expense", password="pass123")
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"rule_name": "越权规则", "expense_type": "office", "limit_amount": 10})
        assert resp.status_code == 403


class TestReimbursements:
    """报销单提交/校验/审批"""

    BASE = "/api/cma/expenses/reimbursements"

    def test_submit_pass_no_rules(self, client: TestClient, db: Session):
        """无规则 → 提交通过，状态 pending"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "entertainment", "title": "客户餐费",
                                 "amount": 500.0, "expense_date": "2026-06-10"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["message"] == "报销单已提交"
        assert data["data"]["auto_check_passed"] is True
        d = data["data"]
        assert d["status"] == "pending"
        assert d["reimb_no"].startswith("BX")
        assert d["expense_type_label"] == "招待费"
        assert d["applicant"] == "测试管理员"  # 默认取当前用户name

    def test_submit_over_single_limit_returned(self, client: TestClient, db: Session):
        """单笔超限 → 自动打回 returned + check_result fail"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待单笔≤100", expense_type="entertainment",
                limit_type="single", limit_amount=100.0)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "entertainment", "title": "大额招待",
                                 "amount": 500.0})
        assert resp.status_code == 200
        data = resp.json()
        assert data["message"] == "报销单超限，已自动打回"
        assert data["data"]["auto_check_passed"] is False
        d = data["data"]
        assert d["status"] == "returned"
        assert d["check_result"] == "fail"
        assert "超限" in d["check_reason"]
        assert d["check_detail"][0]["passed"] is False

    def test_submit_within_single_limit(self, client: TestClient, db: Session):
        """单笔限额内 → 通过，check_detail 有记录"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待单笔≤2000", expense_type="entertainment",
                limit_type="single", limit_amount=2000.0)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "entertainment", "title": "正常招待",
                                 "amount": 500.0})
        assert resp.status_code == 200
        d = resp.json()["data"]
        assert d["status"] == "pending"
        assert d["check_result"] == "pass"
        assert d["check_detail"][0]["passed"] is True

    def test_submit_monthly_limit(self, client: TestClient, db: Session):
        """月度累计超限 → 第二次自动打回"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待月限额1000", expense_type="entertainment",
                limit_type="monthly", limit_amount=1000.0, cycle="monthly")
        r1 = client.post(self.BASE, headers=auth_header(token),
                         json={"expense_type": "entertainment", "title": "第一批",
                               "amount": 500.0, "expense_date": "2026-06-10"})
        assert r1.json()["data"]["auto_check_passed"] is True
        r2 = client.post(self.BASE, headers=auth_header(token),
                         json={"expense_type": "entertainment", "title": "第二批",
                               "amount": 800.0, "expense_date": "2026-06-20"})
        assert r2.status_code == 200
        assert r2.json()["data"]["auto_check_passed"] is False
        assert r2.json()["data"]["status"] == "returned"
        # 累计明细：500 + 800 = 1300
        assert r2.json()["data"]["check_detail"][0]["actual"] == 1300.0

    def test_submit_missing_params(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"title": "没类型没金额"})
        assert resp.status_code == 400
        assert "缺少必要参数" in resp.json()["detail"]

    def test_submit_invalid_type(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "bogus", "title": "x", "amount": 100})
        assert resp.status_code == 400

    def test_submit_zero_amount(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "office", "title": "x", "amount": 0})
        assert resp.status_code == 400
        assert "必须大于0" in resp.json()["detail"]

    def test_submit_bad_date(self, client: TestClient, db: Session):
        """非法日期 → expense_date 为 None 不报错"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(self.BASE, headers=auth_header(token),
                           json={"expense_type": "office", "title": "x", "amount": 100,
                                 "expense_date": "not-a-date"})
        assert resp.status_code == 200
        assert resp.json()["data"]["expense_date"] is None

    def test_list_filters(self, client: TestClient, db: Session):
        """状态/类型/申请人/关键字 过滤"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_reimb(db, reimb_no="BX0001", applicant="张三", title="客户招待", status="pending")
        mk_reimb(db, reimb_no="BX0002", applicant="李四", title="差旅住宿", status="approved")
        mk_reimb(db, reimb_no="BX0003", applicant="张三", title="办公采购", status="rejected",
                 expense_type="office")

        r = client.get(f"{self.BASE}?status=approved", headers=auth_header(token))
        assert [x["reimb_no"] for x in r.json()["data"]] == ["BX0002"]
        r = client.get(f"{self.BASE}?expense_type=office", headers=auth_header(token))
        assert [x["reimb_no"] for x in r.json()["data"]] == ["BX0003"]
        r = client.get(f"{self.BASE}?applicant=张三", headers=auth_header(token))
        assert {x["reimb_no"] for x in r.json()["data"]} == {"BX0001", "BX0003"}
        r = client.get(f"{self.BASE}?keyword=差旅", headers=auth_header(token))
        assert [x["reimb_no"] for x in r.json()["data"]] == ["BX0002"]
        r = client.get(f"{self.BASE}?keyword=BX0003", headers=auth_header(token))
        assert [x["reimb_no"] for x in r.json()["data"]] == ["BX0003"]

    def test_get_detail(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0099")
        resp = client.get(f"{self.BASE}/{reimb.id}", headers=auth_header(token))
        assert resp.status_code == 200
        assert resp.json()["reimb_no"] == "BX0099"

    def test_get_detail_not_found(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.get(f"{self.BASE}/99999", headers=auth_header(token))
        assert resp.status_code == 404

    def test_approve(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0100")
        resp = client.post(f"{self.BASE}/{reimb.id}/approve", headers=auth_header(token),
                           json={"comment": "同意"})
        assert resp.status_code == 200
        assert resp.json()["message"] == "已审批通过"
        d = resp.json()["data"]
        assert d["status"] == "approved"
        assert d["approver"] == "测试管理员"
        assert d["approve_comment"] == "同意"

    def test_approve_wrong_state(self, client: TestClient, db: Session):
        """已审批的单不能再审 → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0101", status="approved")
        resp = client.post(f"{self.BASE}/{reimb.id}/approve", headers=auth_header(token))
        assert resp.status_code == 400
        assert "不可审批" in resp.json()["detail"]

    def test_approve_not_found(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(f"{self.BASE}/99999/approve", headers=auth_header(token))
        assert resp.status_code == 404

    def test_reject_requires_comment(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0102")
        resp = client.post(f"{self.BASE}/{reimb.id}/reject", headers=auth_header(token))
        assert resp.status_code == 400
        assert "必须填写审批意见" in resp.json()["detail"]

    def test_reject_with_comment(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0103")
        resp = client.post(f"{self.BASE}/{reimb.id}/reject", headers=auth_header(token),
                           json={"comment": "发票不合规"})
        assert resp.status_code == 200
        d = resp.json()["data"]
        assert d["status"] == "rejected"
        assert d["approve_comment"] == "发票不合规"

    def test_return(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0104")
        resp = client.post(f"{self.BASE}/{reimb.id}/return", headers=auth_header(token))
        assert resp.status_code == 200
        d = resp.json()["data"]
        assert d["status"] == "returned"
        assert d["approve_comment"] == "人工打回"

    def test_resubmit_after_return(self, client: TestClient, db: Session):
        """打回后改金额重新提交 → 重新校验通过"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待单笔≤100", expense_type="entertainment",
                limit_type="single", limit_amount=100.0)
        # 超限被自动打回
        r = client.post(self.BASE, headers=auth_header(token),
                        json={"expense_type": "entertainment", "title": "超标单",
                              "amount": 500.0})
        rid = r.json()["data"]["id"]
        assert r.json()["data"]["status"] == "returned"
        # 改金额到限额内 → 重新提交通过
        resp = client.post(f"{self.BASE}/{rid}/resubmit", headers=auth_header(token),
                           json={"amount": 80.0})
        assert resp.status_code == 200
        assert resp.json()["message"] == "已重新提交"
        d = resp.json()["data"]
        assert d["status"] == "pending"
        assert d["amount"] == 80.0
        assert d["approver"] is None

    def test_resubmit_still_over(self, client: TestClient, db: Session):
        """重提仍超限 → 再次打回"""
        create_test_user(db)
        token = get_token_for_user(client)
        mk_rule(db, rule_name="招待单笔≤100", expense_type="entertainment",
                limit_type="single", limit_amount=100.0)
        r = client.post(self.BASE, headers=auth_header(token),
                        json={"expense_type": "entertainment", "title": "超标单",
                              "amount": 500.0})
        rid = r.json()["data"]["id"]
        resp = client.post(f"{self.BASE}/{rid}/resubmit", headers=auth_header(token),
                           json={"amount": 300.0})
        assert resp.status_code == 200
        assert resp.json()["message"] == "仍超限，已再次打回"
        assert resp.json()["data"]["status"] == "returned"

    def test_resubmit_wrong_state(self, client: TestClient, db: Session):
        """pending 状态不能重提 → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        reimb = mk_reimb(db, reimb_no="BX0105")
        resp = client.post(f"{self.BASE}/{reimb.id}/resubmit", headers=auth_header(token))
        assert resp.status_code == 400
        assert "不可重新提交" in resp.json()["detail"]

    def test_approve_permission_business_denied(self, client: TestClient, db: Session):
        """business不能审批 → 403"""
        reimb = mk_reimb(db, reimb_no="BX0106")
        u = User(username="biz_approve", password_hash=hashlib.sha256("pass123".encode()).hexdigest(),
                 name="业务员", role="business")
        db.add(u)
        db.commit()
        token = get_token_for_user(client, username="biz_approve", password="pass123")
        resp = client.post(f"{self.BASE}/{reimb.id}/approve", headers=auth_header(token))
        assert resp.status_code == 403


class TestStats:
    """审核看板统计"""

    BASE = "/api/cma/expenses/stats"

    def test_stats_empty(self, client: TestClient, db: Session):
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.get(self.BASE, headers=auth_header(token))
        assert resp.status_code == 200
        data = resp.json()
        assert data["counts"]["total"] == 0
        assert data["monthly_total"] == 0
        assert len(data["budget_usage"]) == 2

    def test_stats_with_data(self, client: TestClient, db: Session):
        """有报销数据 → 状态计数/类型统计/超限预警/月度总额"""
        create_test_user(db)
        token = get_token_for_user(client)
        from datetime import datetime
        mk_reimb(db, reimb_no="BX0201", applicant="张三", amount=500.0,
                 expense_date=datetime(2026, 6, 10), status="pending")
        mk_reimb(db, reimb_no="BX0202", applicant="李四", amount=300.0,
                 expense_date=datetime(2026, 6, 11), status="approved")
        mk_reimb(db, reimb_no="BX0203", applicant="王五", amount=800.0,
                 expense_date=datetime(2026, 5, 20), status="approved")  # 上期不计
        # 超限打回
        mk_reimb(db, reimb_no="BX0204", applicant="赵六", amount=99999.0,
                 expense_date=datetime(2026, 6, 12), status="returned", check_result="fail",
                 check_reason="招待费单笔限额: 超限")
        resp = client.get(f"{self.BASE}?period=2026-06", headers=auth_header(token))
        assert resp.status_code == 200
        data = resp.json()
        assert data["period"] == "2026-06"
        assert data["counts"]["pending"] == 1
        assert data["counts"]["approved"] == 2
        assert data["counts"]["returned"] == 1
        assert data["counts"]["total"] == 4
        # 月度总额 = 500 + 300 = 800（上期不计，returned不计）
        assert data["monthly_total"] == 800.0
        assert data["amounts_by_type"][0]["amount"] == 800.0
        # 超限预警
        assert data["over_limit_count"] == 1
        assert data["over_limit"][0]["reimb_no"] == "BX0204"
        # 预算使用率
        ent_usage = next(u for u in data["budget_usage"] if u["expense_type"] == "entertainment")
        assert ent_usage["used"] == 800.0
        assert ent_usage["usage_rate"] == round(800 / 150000 * 100, 1)
        # 最近列表
        assert len(data["recent"]) == 4

    def test_stats_no_token(self, client: TestClient, db: Session):
        resp = client.get(self.BASE)
        assert resp.status_code == 403
