"""KPI因果链模块测试 — 因果网络 + 模拟推演 + CRUD

覆盖 kpi_causality.py 全部8个端点：
full-network / kpi{id}/network / simulate / list / get / create / update / delete
权限：读需 ceo/finance/business/it，写需 ceo/finance/it
"""
import hashlib

import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session

from app.models import KPIDefinition, KPICausality, KPIValue, Entity, User, UserEntity
from tests.conftest import create_test_user, get_token_for_user, auth_header

BASE = "/api/cma/kpi-causality"


def _seed_kpi(db: Session, code: str, name: str = None, dimension: str = "finance",
              entity_id: int = 1) -> KPIDefinition:
    # entity_id=1 与测试token(账套entity=1)对齐 — 2026-08-23 多租户隔离后必须匹配
    kpi = KPIDefinition(
        kpi_code=code,
        kpi_name=name or code,
        dimension=dimension,
        entity_id=entity_id,
        status="active",
        target_value=100.0,
    )
    db.add(kpi)
    db.commit()
    db.refresh(kpi)
    return kpi


def _seed_entity2(db: Session) -> None:
    """博海(id=2)为主测试实体，酣客(id=1)已有（conftest）"""
    ent = db.query(Entity).filter(Entity.id == 2).first()
    if not ent:
        db.add(Entity(id=2, name="博海网络科技", short_name="博海", status="active"))
        db.commit()


def _seed_chain(db: Session):
    """造一条因果链: 收入 → 净利润 (positive, 0.5)"""
    src = _seed_kpi(db, "BH_REVENUE", "营业收入")
    tgt = _seed_kpi(db, "BH_NET_PROFIT", "净利润")
    c = KPICausality(source_kpi_id=src.id, target_kpi_id=tgt.id,
                     strength=0.5, lag_months=1, direction="positive",
                     formula="净利润 = 收入 × 10%")
    db.add(c)
    db.commit()
    db.refresh(c)
    # 实际值（模拟推演用）
    db.add(KPIValue(kpi_id=src.id, period="2026-07", actual_value=100.0))
    db.add(KPIValue(kpi_id=tgt.id, period="2026-07", actual_value=10.0))
    db.commit()
    return src, tgt, c


class TestFullNetwork:
    def test_empty_network(self, client: TestClient, db: Session):
        """无数据时网络为空"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.get(f"{BASE}/full-network", headers=auth_header(token))
        assert resp.status_code == 200
        data = resp.json()
        assert data["nodes"] == []
        assert data["edges"] == []
        assert data["total_edges"] == 0

    def test_full_network_with_chain(self, client: TestClient, db: Session):
        """有因果链时返回节点和边"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        resp = client.get(f"{BASE}/full-network", headers=auth_header(token))
        assert resp.status_code == 200
        data = resp.json()
        assert data["total_edges"] == 1
        assert data["edges"][0]["source"] == src.id
        assert data["edges"][0]["target"] == tgt.id
        # 两个节点都带KPI信息
        codes = {n["kpi_code"] for n in data["nodes"]}
        assert codes == {"BH_REVENUE", "BH_NET_PROFIT"}


class TestKpiNetwork:
    def test_kpi_network(self, client: TestClient, db: Session):
        """单KPI上下游网络"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        # 源KPI的下游
        resp = client.get(f"{BASE}/kpi/{src.id}/network", headers=auth_header(token))
        assert resp.status_code == 200
        data = resp.json()
        assert data["kpi"]["kpi_code"] == "BH_REVENUE"
        assert len(data["downstream"]) == 1
        assert data["downstream"][0]["kpi_code"] == "BH_NET_PROFIT"
        assert len(data["upstream"]) == 0

        # 目标KPI的上游
        resp2 = client.get(f"{BASE}/kpi/{tgt.id}/network", headers=auth_header(token))
        assert resp2.status_code == 200
        data2 = resp2.json()
        assert len(data2["upstream"]) == 1
        assert data2["upstream"][0]["kpi_code"] == "BH_REVENUE"

    def test_kpi_network_not_found(self, client: TestClient, db: Session):
        """KPI不存在 → 404"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.get(f"{BASE}/kpi/99999/network", headers=auth_header(token))
        assert resp.status_code == 404


class TestSimulate:
    def test_simulate_simple(self, client: TestClient, db: Session):
        """模拟推演：收入+10% → 净利润受影响"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        resp = client.post(f"{BASE}/simulate", headers=auth_header(token), json={
            "kpi_id": src.id,
            "new_value": 110.0,
            "period": "2026-07",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["source"]["kpi_code"] == "BH_REVENUE"
        assert data["source"]["change_pct"] == 10.0
        assert data["total_impacted"] == 1
        impact = data["impacts"][0]
        assert impact["kpi_code"] == "BH_NET_PROFIT"
        # 10% × 0.5(强度) × 1(正向) = 5% 影响
        assert impact["change_pct"] == 5.0
        # 预测值 = 10 × 1.05 = 10.5
        assert impact["predicted_value"] == 10.5

    def test_simulate_negative_direction(self, client: TestClient, db: Session):
        """负向因果：成本↑ → 净利润↓"""
        create_test_user(db)
        token = get_token_for_user(client)
        cost = _seed_kpi(db, "BH_COST", "成本")
        profit = _seed_kpi(db, "BH_NET_PROFIT", "净利润")
        c = KPICausality(source_kpi_id=cost.id, target_kpi_id=profit.id,
                         strength=0.8, lag_months=0, direction="negative")
        db.add(c)
        db.add(KPIValue(kpi_id=cost.id, period="2026-07", actual_value=50.0))
        db.add(KPIValue(kpi_id=profit.id, period="2026-07", actual_value=100.0))
        db.commit()

        resp = client.post(f"{BASE}/simulate", headers=auth_header(token), json={
            "kpi_id": cost.id, "new_value": 60.0, "period": "2026-07",
        })
        assert resp.status_code == 200
        data = resp.json()
        assert data["source"]["change_pct"] == 20.0
        impact = data["impacts"][0]
        # 20% × 0.8 × (-1) = -16%
        assert impact["change_pct"] == -16.0
        # 100 × 0.84 = 84.0
        assert impact["predicted_value"] == 84.0

    def test_simulate_missing_params(self, client: TestClient, db: Session):
        """缺 kpi_id/new_value → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(f"{BASE}/simulate", headers=auth_header(token), json={"kpi_id": 1})
        assert resp.status_code == 400

    def test_simulate_kpi_not_found(self, client: TestClient, db: Session):
        """KPI不存在 → 404"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(f"{BASE}/simulate", headers=auth_header(token),
                           json={"kpi_id": 99999, "new_value": 10.0})
        assert resp.status_code == 404


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

    def test_create_and_get(self, client: TestClient, db: Session):
        """创建因果链 + 按ID查询"""
        create_test_user(db)
        token = get_token_for_user(client)
        src = _seed_kpi(db, "BH_REVENUE", "营业收入")
        tgt = _seed_kpi(db, "BH_NET_PROFIT", "净利润")

        resp = client.post(BASE, headers=auth_header(token), json={
            "source_kpi_id": src.id,
            "target_kpi_id": tgt.id,
            "strength": 0.6,
            "lag_months": 2,
            "direction": "positive",
            "formula": "净利润 = 收入 × 10%",
        })
        assert resp.status_code == 200
        cid = resp.json()["id"]
        assert resp.json()["strength"] == 0.6

        get_resp = client.get(f"{BASE}/{cid}", headers=auth_header(token))
        assert get_resp.status_code == 200
        assert get_resp.json()["source"]["kpi_code"] == "BH_REVENUE"
        assert get_resp.json()["target"]["kpi_code"] == "BH_NET_PROFIT"

    def test_create_duplicate(self, client: TestClient, db: Session):
        """重复创建同一条因果链 → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        src = _seed_kpi(db, "BH_REVENUE", "营业收入")
        tgt = _seed_kpi(db, "BH_NET_PROFIT", "净利润")

        client.post(BASE, headers=auth_header(token),
                    json={"source_kpi_id": src.id, "target_kpi_id": tgt.id})
        resp = client.post(BASE, headers=auth_header(token),
                           json={"source_kpi_id": src.id, "target_kpi_id": tgt.id})
        assert resp.status_code == 400
        assert "已存在" in resp.json()["detail"]

    def test_create_missing_kpis(self, client: TestClient, db: Session):
        """缺源/目标 → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.post(BASE, headers=auth_header(token), json={"source_kpi_id": 1})
        assert resp.status_code == 400

    def test_create_same_kpi(self, client: TestClient, db: Session):
        """源=目标 → 400"""
        create_test_user(db)
        token = get_token_for_user(client)
        src = _seed_kpi(db, "BH_REVENUE", "营业收入")
        resp = client.post(BASE, headers=auth_header(token),
                           json={"source_kpi_id": src.id, "target_kpi_id": src.id})
        assert resp.status_code == 400

    def test_create_kpi_not_found(self, client: TestClient, db: Session):
        """KPI不存在 → 404"""
        create_test_user(db)
        token = get_token_for_user(client)
        src = _seed_kpi(db, "BH_REVENUE", "营业收入")
        resp = client.post(BASE, headers=auth_header(token),
                           json={"source_kpi_id": src.id, "target_kpi_id": 99999})
        assert resp.status_code == 404

    def test_update(self, client: TestClient, db: Session):
        """更新强度/滞后期/方向"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        resp = client.put(f"{BASE}/{c.id}", headers=auth_header(token), json={
            "strength": 0.9, "lag_months": 3, "direction": "negative",
        })
        assert resp.status_code == 200
        assert resp.json()["strength"] == 0.9
        assert resp.json()["lag_months"] == 3
        assert resp.json()["direction"] == "negative"

    def test_update_not_found(self, client: TestClient, db: Session):
        """更新不存在的因果链 → 404"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.put(f"{BASE}/99999", headers=auth_header(token), json={"strength": 0.5})
        assert resp.status_code == 404

    def test_delete(self, client: TestClient, db: Session):
        """删除因果链"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        resp = client.delete(f"{BASE}/{c.id}", headers=auth_header(token))
        assert resp.status_code == 200
        assert resp.json()["message"] == "已删除"

        # 列表验证已删除
        list_resp = client.get(BASE, headers=auth_header(token))
        assert list_resp.json()["total"] == 0

    def test_delete_not_found(self, client: TestClient, db: Session):
        """删除不存在的因果链 → 幂等返回已删除"""
        create_test_user(db)
        token = get_token_for_user(client)
        resp = client.delete(f"{BASE}/99999", headers=auth_header(token))
        assert resp.status_code == 200

    def test_list_filter(self, client: TestClient, db: Session):
        """列表按源/目标KPI过滤"""
        create_test_user(db)
        token = get_token_for_user(client)
        src, tgt, c = _seed_chain(db)

        resp = client.get(f"{BASE}?source_kpi_id={src.id}", headers=auth_header(token))
        assert resp.status_code == 200
        assert resp.json()["total"] == 1

        resp2 = client.get(f"{BASE}?source_kpi_id=99999", headers=auth_header(token))
        assert resp2.json()["total"] == 0


class TestPermissions:
    def test_write_requires_ceo_finance_it(self, client: TestClient, db: Session):
        """business角色无写权限 → 403"""
        # business用户
        business = User(
            username="business_user",
            password_hash=hashlib.sha256("pass123".encode()).hexdigest(),
            name="业务员",
            role="business",
        )
        db.add(business)
        db.commit()
        token = get_token_for_user(client, username="business_user", password="pass123")

        src = _seed_kpi(db, "BH_REVENUE", "营业收入")
        tgt = _seed_kpi(db, "BH_NET_PROFIT", "净利润")
        resp = client.post(BASE, headers=auth_header(token),
                           json={"source_kpi_id": src.id, "target_kpi_id": tgt.id})
        assert resp.status_code == 403

    def test_read_allowed_for_business(self, client: TestClient, db: Session):
        """business角色可读"""
        business = User(
            username="business_user2",
            password_hash=hashlib.sha256("pass123".encode()).hexdigest(),
            name="业务员",
            role="business",
        )
        db.add(business)
        db.commit()
        token = get_token_for_user(client, username="business_user2", password="pass123")

        resp = client.get(BASE, headers=auth_header(token))
        assert resp.status_code == 200

    def test_no_token_denied(self, client: TestClient):
        """无token → 403（router级 require_role 直接拒绝，与 test_reports 约定一致）"""
        resp = client.get(BASE)
        assert resp.status_code == 403
