"""Phase 7.3 — Rewards 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,
    points_cost: int = 50,
    stock_limit: int | None = None,
    max_per_member: int | None = None,
    earn_amount: int = 100,
) -> dict:
    program = await client.post(
        "/api/v1/programs",
        json={"code": f"rwd{suffix}", "name": f"Rewards {suffix}"},
        headers=tenant_headers(TENANT_A),
    )
    assert program.status_code == 201, program.text
    program_id = program.json()["id"]

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

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

    if earn_amount:
        earn = await client.post(
            f"/api/v1/point-accounts/{account_id}/earn",
            json={"amount": earn_amount, "reason": "seed"},
            headers=tenant_headers(TENANT_A),
        )
        assert earn.status_code == 200, earn.text

    reward_payload = {
        "program_id": program_id,
        "code": f"rew{suffix}",
        "name": f"Reward {suffix}",
        "reward_type": "gift",
        "status": "active",
        "points_cost": points_cost,
    }
    if stock_limit is not None:
        reward_payload["stock_limit"] = stock_limit
    if max_per_member is not None:
        reward_payload["max_per_member"] = max_per_member
    reward = await client.post(
        "/api/v1/rewards", json=reward_payload, headers=tenant_headers(TENANT_A)
    )
    assert reward.status_code == 201, reward.text

    return {
        "program_id": program_id,
        "member_id": member_id,
        "account_id": account_id,
        "reward_id": reward.json()["id"],
    }


@pytest.mark.asyncio
async def test_redeem_debits_points_and_creates_redemption(client):
    ctx = await _setup(client, suffix="r1", points_cost=40, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert redeem.status_code == 201, redeem.text
    body = redeem.json()
    assert body["status"] == "pending"
    assert body["points_spent"] == 40
    assert body["voucher_code"]
    assert body["ledger_entry_id"] is not None

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

    reward = await client.get(
        f"/api/v1/rewards/{ctx['reward_id']}", headers=tenant_headers(TENANT_A)
    )
    assert reward.json()["redeemed_count"] == 1

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


@pytest.mark.asyncio
async def test_redeem_free_reward_skips_ledger_debit(client):
    ctx = await _setup(client, suffix="r0", points_cost=0, earn_amount=50)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert redeem.status_code == 201, redeem.text
    body = redeem.json()
    assert body["points_spent"] == 0
    assert body["ledger_entry_id"] is None

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


@pytest.mark.asyncio
async def test_redeem_insufficient_points_rejected(client):
    ctx = await _setup(client, suffix="r2", points_cost=500, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert redeem.status_code == 409
    assert redeem.json()["error"]["code"] == "insufficient_points"

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

    reward = await client.get(
        f"/api/v1/rewards/{ctx['reward_id']}", headers=tenant_headers(TENANT_A)
    )
    assert reward.json()["redeemed_count"] == 0


@pytest.mark.asyncio
async def test_redeem_respects_stock_limit(client):
    ctx = await _setup(client, suffix="r3", points_cost=10, stock_limit=1, earn_amount=100)
    first = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert first.status_code == 201, first.text

    second = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert second.status_code == 409
    assert second.json()["error"]["code"] == "reward_stock_exhausted"


@pytest.mark.asyncio
async def test_redeem_respects_max_per_member(client):
    ctx = await _setup(
        client, suffix="r4", points_cost=10, max_per_member=1, earn_amount=100
    )
    first = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert first.status_code == 201, first.text

    second = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert second.status_code == 409
    assert second.json()["error"]["code"] == "reward_max_per_member_exceeded"


@pytest.mark.asyncio
async def test_redeem_idempotency_returns_same_redemption(client):
    ctx = await _setup(client, suffix="r5", points_cost=10, earn_amount=100)
    first = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"], "idempotency_key": "idem-1"},
        headers=tenant_headers(TENANT_A),
    )
    assert first.status_code == 201, first.text

    second = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"], "idempotency_key": "idem-1"},
        headers=tenant_headers(TENANT_A),
    )
    assert second.status_code == 201
    assert second.json()["id"] == first.json()["id"]

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


@pytest.mark.asyncio
async def test_fulfill_redemption(client):
    ctx = await _setup(client, suffix="r6", points_cost=10, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    redemption_id = redeem.json()["id"]

    fulfill = await client.post(
        f"/api/v1/redemptions/{redemption_id}/fulfill",
        headers=tenant_headers(TENANT_A),
    )
    assert fulfill.status_code == 200, fulfill.text
    assert fulfill.json()["status"] == "fulfilled"
    assert fulfill.json()["fulfilled_at"] is not None

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

    again = await client.post(
        f"/api/v1/redemptions/{redemption_id}/fulfill",
        headers=tenant_headers(TENANT_A),
    )
    assert again.status_code == 409
    assert again.json()["error"]["code"] == "redemption_not_pending"


@pytest.mark.asyncio
async def test_cancel_redemption_refunds_points(client):
    ctx = await _setup(client, suffix="r7", points_cost=30, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    redemption_id = redeem.json()["id"]

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

    cancel = await client.post(
        f"/api/v1/redemptions/{redemption_id}/cancel",
        json={"reason": "customer request"},
        headers=tenant_headers(TENANT_A),
    )
    assert cancel.status_code == 200, cancel.text
    assert cancel.json()["status"] == "cancelled"
    assert cancel.json()["cancelled_at"] is not None
    assert cancel.json()["cancel_reason"] == "customer request"

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

    reward = await client.get(
        f"/api/v1/rewards/{ctx['reward_id']}", headers=tenant_headers(TENANT_A)
    )
    assert reward.json()["redeemed_count"] == 0

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

    again = await client.post(
        f"/api/v1/redemptions/{redemption_id}/cancel",
        json={"reason": "again"},
        headers=tenant_headers(TENANT_A),
    )
    assert again.status_code == 409


@pytest.mark.asyncio
async def test_cancel_requires_reason(client):
    ctx = await _setup(client, suffix="r7b", points_cost=10, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    redemption_id = redeem.json()["id"]
    bad = await client.post(
        f"/api/v1/redemptions/{redemption_id}/cancel",
        json={"reason": ""},
        headers=tenant_headers(TENANT_A),
    )
    assert bad.status_code == 422


@pytest.mark.asyncio
async def test_inactive_reward_rejected(client):
    ctx = await _setup(client, suffix="r8", points_cost=10, earn_amount=100)
    patched = await client.patch(
        f"/api/v1/rewards/{ctx['reward_id']}",
        json={"status": "inactive"},
        headers=tenant_headers(TENANT_A),
    )
    assert patched.status_code == 200, patched.text

    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert redeem.status_code == 409
    assert redeem.json()["error"]["code"] == "reward_not_active"


@pytest.mark.asyncio
async def test_list_redemptions_for_reward_and_top_level(client):
    ctx = await _setup(client, suffix="r9", points_cost=10, earn_amount=100)
    redeem = await client.post(
        f"/api/v1/rewards/{ctx['reward_id']}/redeem",
        json={"member_id": ctx["member_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert redeem.status_code == 201, redeem.text

    listed = await client.get(
        f"/api/v1/rewards/{ctx['reward_id']}/redemptions",
        headers=tenant_headers(TENANT_A),
    )
    assert listed.status_code == 200
    assert listed.json()["total"] >= 1

    top_level = await client.get(
        "/api/v1/redemptions",
        params={"reward_id": ctx["reward_id"]},
        headers=tenant_headers(TENANT_A),
    )
    assert top_level.status_code == 200
    assert top_level.json()["total"] >= 1

    fetched = await client.get(
        f"/api/v1/redemptions/{redeem.json()['id']}",
        headers=tenant_headers(TENANT_A),
    )
    assert fetched.status_code == 200
    assert fetched.json()["id"] == redeem.json()["id"]
