"""Phase 7.5 — Referral Engine tests."""
from __future__ import annotations

import pytest

from app.events.publisher import get_event_publisher
from app.events.types import LoyaltyEventType
from app.tests.conftest import TENANT_A, tenant_headers


async def _setup(
    client,
    *,
    suffix: str,
    referrer_bonus: int = 50,
    referee_bonus: int = 20,
    max_referrals: int | None = None,
    enroll_referee: bool = False,
) -> dict:
    program = await client.post(
        "/api/v1/programs",
        json={"code": f"ref{suffix}", "name": f"Referral {suffix}"},
        headers=tenant_headers(TENANT_A),
    )
    assert program.status_code == 201, program.text
    program_id = program.json()["id"]

    referrer = await client.post(
        "/api/v1/members",
        json={"program_id": program_id, "display_name": "Referrer", "enroll": True},
        headers=tenant_headers(TENANT_A),
    )
    assert referrer.status_code == 201, referrer.text
    referrer_id = referrer.json()["id"]

    referrer_account = await client.post(
        "/api/v1/point-accounts",
        json={"program_id": program_id, "member_id": referrer_id},
        headers=tenant_headers(TENANT_A),
    )
    assert referrer_account.status_code == 201, referrer_account.text
    referrer_account_id = referrer_account.json()["id"]

    referee = await client.post(
        "/api/v1/members",
        json={
            "program_id": program_id,
            "display_name": "Referee",
            "enroll": enroll_referee,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert referee.status_code == 201, referee.text
    referee_id = referee.json()["id"]

    referee_account = await client.post(
        "/api/v1/point-accounts",
        json={"program_id": program_id, "member_id": referee_id},
        headers=tenant_headers(TENANT_A),
    )
    assert referee_account.status_code == 201, referee_account.text
    referee_account_id = referee_account.json()["id"]

    referral_program = await client.post(
        "/api/v1/referral-programs",
        json={
            "program_id": program_id,
            "code": f"rp{suffix}",
            "name": f"Referral Program {suffix}",
            "status": "active",
            "referrer_bonus_points": referrer_bonus,
            "referee_bonus_points": referee_bonus,
            "max_referrals_per_member": max_referrals,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert referral_program.status_code == 201, referral_program.text
    referral_program_id = referral_program.json()["id"]

    code_resp = await client.post(
        "/api/v1/referral-codes",
        json={"referral_program_id": referral_program_id, "member_id": referrer_id},
        headers=tenant_headers(TENANT_A),
    )
    assert code_resp.status_code == 201, code_resp.text

    return {
        "program_id": program_id,
        "referral_program_id": referral_program_id,
        "referrer_id": referrer_id,
        "referrer_account_id": referrer_account_id,
        "referee_id": referee_id,
        "referee_account_id": referee_account_id,
        "code": code_resp.json()["code"],
        "code_id": code_resp.json()["id"],
    }


async def _attribute(client, ctx, **overrides):
    payload = {"code": ctx["code"], "referee_member_id": ctx["referee_id"]}
    payload.update(overrides)
    return await client.post(
        "/api/v1/referrals/attribute", json=payload, headers=tenant_headers(TENANT_A)
    )


@pytest.mark.asyncio
async def test_referral_program_crud(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "refcrud", "name": "Referral CRUD Program"},
        headers=tenant_headers(TENANT_A),
    )
    program_id = program.json()["id"]

    created = await client.post(
        "/api/v1/referral-programs",
        json={
            "program_id": program_id,
            "code": "rpcrud",
            "name": "CRUD",
            "referrer_bonus_points": 10,
            "referee_bonus_points": 5,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert created.status_code == 201, created.text
    assert created.json()["status"] == "draft"
    rp_id = created.json()["id"]

    listed = await client.get(
        "/api/v1/referral-programs", headers=tenant_headers(TENANT_A)
    )
    assert listed.status_code == 200
    assert any(p["id"] == rp_id for p in listed.json())

    fetched = await client.get(
        f"/api/v1/referral-programs/{rp_id}", headers=tenant_headers(TENANT_A)
    )
    assert fetched.status_code == 200
    assert fetched.json()["code"] == "RPCRUD"

    updated = await client.patch(
        f"/api/v1/referral-programs/{rp_id}",
        json={"status": "active", "referrer_bonus_points": 25},
        headers=tenant_headers(TENANT_A),
    )
    assert updated.status_code == 200, updated.text
    assert updated.json()["status"] == "active"
    assert updated.json()["referrer_bonus_points"] == 25

    deleted = await client.post(
        f"/api/v1/referral-programs/{rp_id}/delete", headers=tenant_headers(TENANT_A)
    )
    assert deleted.status_code == 200
    assert deleted.json()["is_deleted"] is True

    events = {e.event_type for e in get_event_publisher().published}
    assert LoyaltyEventType.REFERRAL_PROGRAM_CREATED.value in events
    assert LoyaltyEventType.REFERRAL_PROGRAM_UPDATED.value in events
    assert LoyaltyEventType.REFERRAL_PROGRAM_DELETED.value in events


@pytest.mark.asyncio
async def test_issue_code_returns_existing_active_code(client):
    ctx = await _setup(client, suffix="ic1")

    second = await client.post(
        "/api/v1/referral-codes",
        json={
            "referral_program_id": ctx["referral_program_id"],
            "member_id": ctx["referrer_id"],
        },
        headers=tenant_headers(TENANT_A),
    )
    assert second.status_code == 201, second.text
    assert second.json()["id"] == ctx["code_id"]

    events = {e.event_type for e in get_event_publisher().published}
    assert LoyaltyEventType.REFERRAL_CODE_ISSUED.value in events


@pytest.mark.asyncio
async def test_full_flow_attribute_convert_reward_with_ledger_balances(client):
    ctx = await _setup(
        client, suffix="h1", referrer_bonus=50, referee_bonus=20, enroll_referee=False
    )

    attributed = await _attribute(client, ctx)
    assert attributed.status_code == 201, attributed.text
    body = attributed.json()
    assert body["status"] == "pending"
    attribution_id = body["id"]

    # Referee must be active before conversion.
    enroll = await client.post(
        f"/api/v1/members/{ctx['referee_id']}/enroll",
        json={},
        headers=tenant_headers(TENANT_A),
    )
    assert enroll.status_code == 200, enroll.text

    converted = await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    assert converted.status_code == 200, converted.text
    assert converted.json()["status"] == "converted"
    assert converted.json()["converted_at"] is not None

    rewarded = await client.post(
        f"/api/v1/referrals/{attribution_id}/reward",
        headers=tenant_headers(TENANT_A),
    )
    assert rewarded.status_code == 200, rewarded.text
    reward_body = rewarded.json()
    assert reward_body["status"] == "rewarded"
    assert reward_body["referrer_ledger_entry_id"] is not None
    assert reward_body["referee_ledger_entry_id"] is not None

    referrer_balance = await client.get(
        f"/api/v1/point-accounts/{ctx['referrer_account_id']}/balance",
        headers=tenant_headers(TENANT_A),
    )
    assert referrer_balance.json()["balance"] == 50

    referee_balance = await client.get(
        f"/api/v1/point-accounts/{ctx['referee_account_id']}/balance",
        headers=tenant_headers(TENANT_A),
    )
    assert referee_balance.json()["balance"] == 20

    events = {e.event_type for e in get_event_publisher().published}
    assert LoyaltyEventType.REFERRAL_CODE_ISSUED.value in events
    assert LoyaltyEventType.REFERRAL_ATTRIBUTED.value in events
    assert LoyaltyEventType.REFERRAL_CONVERTED.value in events
    assert LoyaltyEventType.REFERRAL_REWARDED.value in events
    assert LoyaltyEventType.POINTS_EARNED.value in events


@pytest.mark.asyncio
async def test_self_referral_rejected(client):
    ctx = await _setup(client, suffix="sr1")
    resp = await _attribute(client, ctx, referee_member_id=ctx["referrer_id"])
    assert resp.status_code == 422
    assert resp.json()["error"]["code"] == "referral_self_referral_forbidden"


@pytest.mark.asyncio
async def test_referee_already_attributed_rejected(client):
    ctx = await _setup(client, suffix="dup1")
    first = await _attribute(client, ctx)
    assert first.status_code == 201, first.text

    second = await _attribute(client, ctx)
    assert second.status_code == 409
    assert second.json()["error"]["code"] == "referral_referee_already_attributed"


@pytest.mark.asyncio
async def test_max_referrals_per_member_enforced(client):
    ctx = await _setup(client, suffix="max1", max_referrals=1)

    first = await _attribute(client, ctx)
    assert first.status_code == 201, first.text

    # New referee for the same referrer's code.
    referee2 = await client.post(
        "/api/v1/members",
        json={
            "program_id": ctx["program_id"],
            "display_name": "Referee 2",
            "enroll": True,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert referee2.status_code == 201, referee2.text

    second = await _attribute(client, ctx, referee_member_id=referee2.json()["id"])
    assert second.status_code == 409
    assert second.json()["error"]["code"] == "referral_max_referrals_exceeded"


@pytest.mark.asyncio
async def test_convert_requires_pending_status(client):
    ctx = await _setup(client, suffix="cv1", enroll_referee=True)
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]

    first_convert = await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    assert first_convert.status_code == 200, first_convert.text

    second_convert = await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    assert second_convert.status_code == 409
    assert second_convert.json()["error"]["code"] == "referral_not_pending"


@pytest.mark.asyncio
async def test_convert_requires_referee_active(client):
    ctx = await _setup(client, suffix="cv2", enroll_referee=False)
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]

    resp = await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == "referral_referee_not_active"


@pytest.mark.asyncio
async def test_reward_requires_converted_status(client):
    ctx = await _setup(client, suffix="rw1", enroll_referee=True)
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]

    resp = await client.post(
        f"/api/v1/referrals/{attribution_id}/reward",
        headers=tenant_headers(TENANT_A),
    )
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == "referral_not_converted"


@pytest.mark.asyncio
async def test_double_reward_rejected(client):
    ctx = await _setup(client, suffix="dr1", enroll_referee=True)
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]

    await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    first_reward = await client.post(
        f"/api/v1/referrals/{attribution_id}/reward",
        headers=tenant_headers(TENANT_A),
    )
    assert first_reward.status_code == 200, first_reward.text

    second_reward = await client.post(
        f"/api/v1/referrals/{attribution_id}/reward",
        headers=tenant_headers(TENANT_A),
    )
    assert second_reward.status_code == 409
    assert second_reward.json()["error"]["code"] == "referral_not_converted"

    referrer_balance = await client.get(
        f"/api/v1/point-accounts/{ctx['referrer_account_id']}/balance",
        headers=tenant_headers(TENANT_A),
    )
    assert referrer_balance.json()["balance"] == 50


@pytest.mark.asyncio
async def test_attribute_idempotency_returns_same_attribution(client):
    ctx = await _setup(client, suffix="idem1")

    first = await _attribute(client, ctx, idempotency_key="ref-idem-1")
    assert first.status_code == 201, first.text

    second = await _attribute(client, ctx, idempotency_key="ref-idem-1")
    assert second.status_code == 201, second.text
    assert second.json()["id"] == first.json()["id"]


@pytest.mark.asyncio
async def test_cancel_pending_attribution(client):
    ctx = await _setup(client, suffix="cn1")
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]

    cancelled = await client.post(
        f"/api/v1/referrals/{attribution_id}/cancel",
        json={"reason": "duplicate signup"},
        headers=tenant_headers(TENANT_A),
    )
    assert cancelled.status_code == 200, cancelled.text
    assert cancelled.json()["status"] == "cancelled"
    assert cancelled.json()["cancel_reason"] == "duplicate signup"

    # Cancelling again should be rejected.
    again = await client.post(
        f"/api/v1/referrals/{attribution_id}/cancel",
        json={},
        headers=tenant_headers(TENANT_A),
    )
    assert again.status_code == 409
    assert again.json()["error"]["code"] == "referral_not_cancellable"

    events = {e.event_type for e in get_event_publisher().published}
    assert LoyaltyEventType.REFERRAL_CANCELLED.value in events


@pytest.mark.asyncio
async def test_rewarded_attribution_cannot_be_cancelled(client):
    ctx = await _setup(client, suffix="cn2", enroll_referee=True)
    attributed = await _attribute(client, ctx)
    attribution_id = attributed.json()["id"]
    await client.post(
        f"/api/v1/referrals/{attribution_id}/convert",
        headers=tenant_headers(TENANT_A),
    )
    await client.post(
        f"/api/v1/referrals/{attribution_id}/reward",
        headers=tenant_headers(TENANT_A),
    )

    resp = await client.post(
        f"/api/v1/referrals/{attribution_id}/cancel",
        json={},
        headers=tenant_headers(TENANT_A),
    )
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == "referral_not_cancellable"


@pytest.mark.asyncio
async def test_list_referral_attributions(client):
    ctx = await _setup(client, suffix="ls1")
    attributed = await _attribute(client, ctx)
    assert attributed.status_code == 201, attributed.text

    listed = await client.get(
        "/api/v1/referrals",
        params={"referral_program_id": ctx["referral_program_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert listed.status_code == 200, listed.text
    assert listed.json()["total"] >= 1
    assert listed.json()["items"][0]["referral_program_id"] == ctx["referral_program_id"]


@pytest.mark.asyncio
async def test_attribute_rejects_inactive_referral_program(client):
    ctx = await _setup(client, suffix="ia1")
    deactivated = await client.patch(
        f"/api/v1/referral-programs/{ctx['referral_program_id']}",
        json={"status": "inactive"},
        headers=tenant_headers(TENANT_A),
    )
    assert deactivated.status_code == 200, deactivated.text

    resp = await _attribute(client, ctx)
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == "referral_program_not_active"


@pytest.mark.asyncio
async def test_revoked_code_cannot_be_used(client):
    ctx = await _setup(client, suffix="rc1")
    revoked = await client.post(
        f"/api/v1/referral-codes/{ctx['code_id']}/revoke",
        headers=tenant_headers(TENANT_A),
    )
    assert revoked.status_code == 200
    assert revoked.json()["status"] == "revoked"

    resp = await _attribute(client, ctx)
    assert resp.status_code == 409
    assert resp.json()["error"]["code"] == "referral_code_not_active"
