"""Loyalty API, service rules, events, and audit tests — Phase 7.0."""
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


@pytest.mark.asyncio
async def test_health(client):
    resp = await client.get("/health")
    assert resp.status_code == 200
    body = resp.json()
    assert body["service"] == "loyalty-service"
    assert body["status"] == "ok"
    assert body["version"] == "0.7.6.0"


@pytest.mark.asyncio
async def test_program_member_enroll_point_account_flow(client):
    program = await client.post(
        "/api/v1/programs",
        json={
            "code": "default",
            "name": "Default Loyalty",
            "status": "active",
            "is_default": True,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert program.status_code == 201, program.text
    program_id = program.json()["id"]
    assert program.json()["code"] == "DEFAULT"

    tier = await client.post(
        "/api/v1/tiers",
        json={
            "program_id": program_id,
            "code": "bronze",
            "name": "Bronze",
            "rank": 1,
            "min_points": 0,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert tier.status_code == 201, tier.text
    tier_id = tier.json()["id"]

    member = await client.post(
        "/api/v1/members",
        json={
            "program_id": program_id,
            "display_name": "Sara Ahmadi",
            "email": "sara@example.com",
            "mobile": "+989121234567",
            "tier_id": tier_id,
            "enroll": True,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert member.status_code == 201, member.text
    member_body = member.json()
    assert member_body["status"] == "active"
    assert member_body["enrolled_at"] is not None
    assert member_body["membership_number"]
    member_id = member_body["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
    assert "balance" not in account.json()

    events = get_event_publisher().published
    types = {e.event_type for e in events}
    assert LoyaltyEventType.PROGRAM_CREATED.value in types
    assert LoyaltyEventType.TIER_CREATED.value in types
    assert LoyaltyEventType.MEMBER_CREATED.value in types
    assert LoyaltyEventType.MEMBER_ENROLLED.value in types
    assert LoyaltyEventType.POINT_ACCOUNT_OPENED.value in types


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

    reward = await client.post(
        "/api/v1/rewards",
        json={
            "program_id": program_id,
            "code": "free-coffee",
            "name": "Free Coffee",
            "reward_type": "gift",
            "points_cost": 100,
            "status": "active",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert reward.status_code == 201, reward.text

    campaign = await client.post(
        "/api/v1/campaigns",
        json={
            "program_id": program_id,
            "code": "summer",
            "name": "Summer Boost",
            "rules": {"multiplier": 2},
            "starts_on": "2026-06-01",
            "ends_on": "2026-08-31",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert campaign.status_code == 201, campaign.text
    campaign_id = campaign.json()["id"]
    assert campaign.json()["rule_version"] == 1

    updated = await client.patch(
        f"/api/v1/campaigns/{campaign_id}",
        json={"rules": {"multiplier": 3}, "bump_rule_version": True},
        headers=tenant_headers(TENANT_A),
    )
    assert updated.status_code == 200, updated.text
    assert updated.json()["rule_version"] == 2


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

    ok = await client.patch(
        f"/api/v1/programs/{program_id}",
        json={"name": "Updated", "version": version},
        headers=tenant_headers(TENANT_A),
    )
    assert ok.status_code == 200
    assert ok.json()["version"] == version + 1

    conflict = await client.patch(
        f"/api/v1/programs/{program_id}",
        json={"name": "Stale", "version": version},
        headers=tenant_headers(TENANT_A),
    )
    assert conflict.status_code == 409


@pytest.mark.asyncio
async def test_soft_delete_releases_code_for_recreate(client):
    create = await client.post(
        "/api/v1/programs",
        json={"code": "reuse", "name": "Reusable"},
        headers=tenant_headers(TENANT_A),
    )
    assert create.status_code == 201, create.text
    program_id = create.json()["id"]

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

    recreate = await client.post(
        "/api/v1/programs",
        json={"code": "reuse", "name": "Reusable Again"},
        headers=tenant_headers(TENANT_A),
    )
    assert recreate.status_code == 201, recreate.text
    assert recreate.json()["code"] == "REUSE"


@pytest.mark.asyncio
async def test_audit_log_and_outbox_recorded(client):
    from sqlalchemy import select

    from app.core.database import AsyncSessionLocal
    from app.models.outbox import OutboxEvent

    program = await client.post(
        "/api/v1/programs",
        json={"code": "audit1", "name": "Audit Program"},
        headers=tenant_headers(TENANT_A),
    )
    assert program.status_code == 201, program.text
    program_id = program.json()["id"]

    audit = await client.get(
        "/api/v1/audit",
        params={"entity_type": "loyalty_program", "entity_id": program_id},
        headers=tenant_headers(TENANT_A),
    )
    assert audit.status_code == 200, audit.text
    assert any(row["action"] == "create" for row in audit.json())

    events = get_event_publisher().published
    assert any(e.event_type == LoyaltyEventType.PROGRAM_CREATED.value for e in events)

    async with AsyncSessionLocal() as session:
        rows = (await session.execute(select(OutboxEvent))).scalars().all()
    assert any(
        row.event_type == LoyaltyEventType.PROGRAM_CREATED.value
        and str(row.status).lower().endswith("pending")
        for row in rows
    )


@pytest.mark.asyncio
async def test_null_required_field_on_update_rejected(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "nullu", "name": "Null Update"},
        headers=tenant_headers(TENANT_A),
    )
    assert program.status_code == 201
    program_id = program.json()["id"]
    version = program.json()["version"]
    bad = await client.patch(
        f"/api/v1/programs/{program_id}",
        json={"name": None, "version": version},
        headers=tenant_headers(TENANT_A),
    )
    assert bad.status_code == 422
    assert bad.json()["error"]["code"] == "null_not_allowed"


@pytest.mark.asyncio
async def test_member_soft_delete_releases_membership_number(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "mbrdel", "name": "Member Delete"},
        headers=tenant_headers(TENANT_A),
    )
    program_id = program.json()["id"]
    member = await client.post(
        "/api/v1/members",
        json={
            "program_id": program_id,
            "display_name": "A",
            "membership_number": "MBR-REUSE-1",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert member.status_code == 201, member.text
    member_id = member.json()["id"]
    deleted = await client.post(
        f"/api/v1/members/{member_id}/delete",
        headers=tenant_headers(TENANT_A),
    )
    assert deleted.status_code == 200
    recreate = await client.post(
        "/api/v1/members",
        json={
            "program_id": program_id,
            "display_name": "B",
            "membership_number": "MBR-REUSE-1",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert recreate.status_code == 201, recreate.text


@pytest.mark.asyncio
async def test_member_update_with_tier_persists_audit(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "tieru", "name": "Tier Update"},
        headers=tenant_headers(TENANT_A),
    )
    program_id = program.json()["id"]
    tier = await client.post(
        "/api/v1/tiers",
        json={
            "program_id": program_id,
            "code": "gold",
            "name": "Gold",
            "rank": 2,
            "min_points": 100,
        },
        headers=tenant_headers(TENANT_A),
    )
    tier_id = tier.json()["id"]
    member = await client.post(
        "/api/v1/members",
        json={"program_id": program_id, "display_name": "C"},
        headers=tenant_headers(TENANT_A),
    )
    member_id = member.json()["id"]
    version = member.json()["version"]
    updated = await client.patch(
        f"/api/v1/members/{member_id}",
        json={"tier_id": tier_id, "version": version},
        headers=tenant_headers(TENANT_A),
    )
    assert updated.status_code == 200, updated.text
    audit = await client.get(
        "/api/v1/audit",
        params={"entity_type": "member", "entity_id": member_id},
        headers=tenant_headers(TENANT_A),
    )
    assert audit.status_code == 200
    assert any(row["action"] == "update" for row in audit.json())


@pytest.mark.asyncio
async def test_point_account_rejects_balance_extra_field(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "bal", "name": "Balance Guard"},
        headers=tenant_headers(TENANT_A),
    )
    program_id = program.json()["id"]
    member = await client.post(
        "/api/v1/members",
        json={"program_id": program_id, "display_name": "M", "enroll": True},
        headers=tenant_headers(TENANT_A),
    )
    member_id = member.json()["id"]
    bad = await client.post(
        "/api/v1/point-accounts",
        json={
            "program_id": program_id,
            "member_id": member_id,
            "balance": 100,
        },
        headers=tenant_headers(TENANT_A),
    )
    assert bad.status_code == 422


@pytest.mark.asyncio
async def test_default_program_exclusivity(client):
    a = await client.post(
        "/api/v1/programs",
        json={"code": "d1", "name": "D1", "is_default": True},
        headers=tenant_headers(TENANT_A),
    )
    b = await client.post(
        "/api/v1/programs",
        json={"code": "d2", "name": "D2", "is_default": True},
        headers=tenant_headers(TENANT_A),
    )
    assert a.status_code == 201
    assert b.status_code == 201
    listed = await client.get("/api/v1/programs", headers=tenant_headers(TENANT_A))
    defaults = [p for p in listed.json() if p["is_default"]]
    assert len(defaults) == 1
    assert defaults[0]["code"] == "D2"


@pytest.mark.asyncio
async def test_invalid_campaign_dates(client):
    program = await client.post(
        "/api/v1/programs",
        json={"code": "dates", "name": "Dates Program"},
        headers=tenant_headers(TENANT_A),
    )
    program_id = program.json()["id"]
    bad = await client.post(
        "/api/v1/campaigns",
        json={
            "program_id": program_id,
            "code": "bad",
            "name": "Bad Dates",
            "starts_on": "2026-12-01",
            "ends_on": "2026-01-01",
        },
        headers=tenant_headers(TENANT_A),
    )
    assert bad.status_code == 422
