"""Phase 7.1 — Membership 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_B, tenant_headers
from app.validators.membership import ensure_lifecycle_transition
from app.models.types import MemberStatus, MembershipLifecycleAction
from shared.exceptions import AppError


async def _program(client, code="p71", **extra):
    resp = await client.post(
        "/api/v1/programs",
        json={"code": code, "name": code, **extra},
        headers=tenant_headers(TENANT_A),
    )
    assert resp.status_code == 201, resp.text
    return resp.json()


async def _member(client, program_id, *, enroll=False, display_name="Ali"):
    resp = await client.post(
        "/api/v1/members",
        json={
            "program_id": program_id,
            "display_name": display_name,
            "enroll": enroll,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert resp.status_code == 201, resp.text
    return resp.json()


def test_lifecycle_transition_matrix_unit():
    ensure_lifecycle_transition(
        action=MembershipLifecycleAction.ACTIVATE, current=MemberStatus.PENDING
    )
    with pytest.raises(AppError) as exc:
        ensure_lifecycle_transition(
            action=MembershipLifecycleAction.FREEZE, current=MemberStatus.PENDING
        )
    assert exc.value.error_code == "invalid_lifecycle_transition"
    with pytest.raises(AppError) as exc2:
        ensure_lifecycle_transition(
            action=MembershipLifecycleAction.ACTIVATE, current=MemberStatus.CANCELLED
        )
    assert exc2.value.error_code == "membership_terminal"


@pytest.mark.asyncio
async def test_activate_freeze_resume_renew_cancel_flow(client):
    program = await _program(client, "life1")
    member = await _member(client, program["id"], enroll=False)
    mid = member["id"]

    activated = await client.post(
        f"/api/v1/members/{mid}/activate",
        json={"term_days": 30},
        headers=tenant_headers(TENANT_A),
    )
    assert activated.status_code == 200, activated.text
    body = activated.json()
    assert body["status"] == "active"
    assert body["activated_at"] is not None
    assert body["membership_expires_at"] is not None

    frozen = await client.post(
        f"/api/v1/members/{mid}/freeze",
        json={"reason": "vacation"},
        headers=tenant_headers(TENANT_A),
    )
    assert frozen.status_code == 200
    assert frozen.json()["status"] == "frozen"
    assert frozen.json()["freeze_reason"] == "vacation"

    resumed = await client.post(
        f"/api/v1/members/{mid}/resume",
        json={},
        headers=tenant_headers(TENANT_A),
    )
    assert resumed.status_code == 200
    assert resumed.json()["status"] == "active"
    assert resumed.json()["frozen_at"] is None

    renewed = await client.post(
        f"/api/v1/members/{mid}/renew",
        json={"term_days": 60},
        headers=tenant_headers(TENANT_A),
    )
    assert renewed.status_code == 200
    assert renewed.json()["status"] == "active"

    cancelled = await client.post(
        f"/api/v1/members/{mid}/cancel",
        json={"reason": "customer request"},
        headers=tenant_headers(TENANT_A),
    )
    assert cancelled.status_code == 200
    assert cancelled.json()["status"] == "cancelled"

    life = await client.get(
        f"/api/v1/members/{mid}/lifecycle",
        headers=tenant_headers(TENANT_A),
    )
    assert life.status_code == 200
    actions = {row["action"] for row in life.json()}
    assert {"activate", "freeze", "resume", "renew", "cancel"} <= actions

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


@pytest.mark.asyncio
async def test_expire_and_reactivate(client):
    program = await _program(client, "exp1")
    member = await _member(client, program["id"], enroll=True)
    mid = member["id"]

    expired = await client.post(
        f"/api/v1/members/{mid}/expire",
        json={},
        headers=tenant_headers(TENANT_A),
    )
    assert expired.status_code == 200
    assert expired.json()["status"] == "expired"

    reactivated = await client.post(
        f"/api/v1/members/{mid}/activate",
        json={"term_days": 10},
        headers=tenant_headers(TENANT_A),
    )
    assert reactivated.status_code == 200
    assert reactivated.json()["status"] == "active"
    assert reactivated.json()["expired_at"] is None


@pytest.mark.asyncio
async def test_transfer_creates_target_member(client):
    source_program = await _program(client, "src")
    target_program = await _program(client, "dst")
    member = await _member(client, source_program["id"], enroll=True)
    mid = member["id"]

    transferred = await client.post(
        f"/api/v1/members/{mid}/transfer",
        json={"target_program_id": target_program["id"], "reason": "move"},
        headers=tenant_headers(TENANT_A),
    )
    assert transferred.status_code == 200, transferred.text
    assert transferred.json()["status"] == "transferred"
    target_id = transferred.json()["transferred_to_member_id"]
    assert target_id

    target = await client.get(
        f"/api/v1/members/{target_id}",
        headers=tenant_headers(TENANT_A),
    )
    assert target.status_code == 200
    assert target.json()["program_id"] == target_program["id"]
    assert target.json()["status"] == "active"


@pytest.mark.asyncio
async def test_invalid_transition_rejected(client):
    program = await _program(client, "bad")
    member = await _member(client, program["id"], enroll=False)
    mid = member["id"]
    bad = await client.post(
        f"/api/v1/members/{mid}/freeze",
        json={"reason": "nope"},
        headers=tenant_headers(TENANT_A),
    )
    assert bad.status_code == 409
    assert bad.json()["error"]["code"] == "invalid_lifecycle_transition"


@pytest.mark.asyncio
async def test_program_delete_blocked_with_active_members(client):
    program = await _program(client, "block")
    await _member(client, program["id"], enroll=True)
    deleted = await client.post(
        f"/api/v1/programs/{program['id']}/delete",
        headers=tenant_headers(TENANT_A),
    )
    assert deleted.status_code == 409
    assert deleted.json()["error"]["code"] == "program_has_active_members"


@pytest.mark.asyncio
async def test_lifecycle_tenant_isolation(client):
    program = await _program(client, "iso")
    member = await _member(client, program["id"], enroll=True)
    mid = member["id"]
    other = await client.get(
        f"/api/v1/members/{mid}/lifecycle",
        headers=tenant_headers(TENANT_B),
    )
    assert other.status_code == 404
