# app/modules/loyalty/tests/integration/test_merchant_api.py
"""
Integration tests for merchant loyalty API endpoints.

Tests the merchant program CRUD endpoints at:
    /api/v1/merchants/loyalty/*

Authentication: Uses dependency overrides (merch_auth_headers pattern).
"""

import pytest

BASE = "/api/v1/merchants/loyalty"


# ============================================================================
# GET /program
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantGetProgram:
    """Tests for GET /api/v1/merchants/loyalty/program."""

    def test_get_program_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns the merchant's loyalty program."""
        response = client.get(
            f"{BASE}/program", headers=loyalty_merchant_headers
        )
        assert response.status_code == 200
        data = response.json()
        assert data["id"] == loyalty_store_setup["program"].id
        assert data["merchant_id"] == loyalty_store_setup["merchant"].id
        assert data["points_per_euro"] == 10
        assert data["is_active"] is True

    def test_get_program_includes_display_fields(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Response includes computed display fields."""
        response = client.get(
            f"{BASE}/program", headers=loyalty_merchant_headers
        )
        assert response.status_code == 200
        data = response.json()
        assert "display_name" in data
        assert "is_stamps_enabled" in data
        assert "is_points_enabled" in data

    def test_get_program_not_found(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Returns 404 when merchant has no program."""
        response = client.get(
            f"{BASE}/program", headers=loyalty_merchant_headers_no_program
        )
        assert response.status_code == 404


# ============================================================================
# POST /program
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantCreateProgram:
    """Tests for POST /api/v1/merchants/loyalty/program."""

    def test_create_program_success(
        self, client, loyalty_merchant_headers_no_program, loyalty_merchant_setup
    ):
        """Create a new loyalty program for the merchant."""
        response = client.post(
            f"{BASE}/program",
            json={
                "loyalty_type": "points",
                "points_per_euro": 5,
                "card_name": "My Rewards",
                "card_color": "#FF5733",
            },
            headers=loyalty_merchant_headers_no_program,
        )
        assert response.status_code == 201
        data = response.json()
        assert data["loyalty_type"] == "points"
        assert data["points_per_euro"] == 5
        assert data["card_name"] == "My Rewards"
        assert data["card_color"] == "#FF5733"
        assert data["merchant_id"] == loyalty_merchant_setup["merchant"].id

    def test_create_program_with_rewards(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Create program with point rewards configured."""
        response = client.post(
            f"{BASE}/program",
            json={
                "loyalty_type": "points",
                "points_per_euro": 10,
                "points_rewards": [
                    {
                        "id": "r1",
                        "name": "5 EUR off",
                        "points_required": 100,
                        "is_active": True,
                    },
                    {
                        "id": "r2",
                        "name": "10 EUR off",
                        "points_required": 200,
                        "is_active": True,
                    },
                ],
            },
            headers=loyalty_merchant_headers_no_program,
        )
        assert response.status_code == 201
        data = response.json()
        assert len(data["points_rewards"]) == 2
        assert data["points_rewards"][0]["name"] == "5 EUR off"

    def test_create_program_defaults(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Creating with minimal data uses sensible defaults."""
        response = client.post(
            f"{BASE}/program",
            json={"loyalty_type": "points"},
            headers=loyalty_merchant_headers_no_program,
        )
        assert response.status_code == 201
        data = response.json()
        assert data["points_per_euro"] == 1
        assert data["is_active"] is True

    def test_create_program_duplicate_fails(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Cannot create a second program for the same merchant."""
        response = client.post(
            f"{BASE}/program",
            json={"loyalty_type": "points"},
            headers=loyalty_merchant_headers,
        )
        # Should fail - merchant already has a program
        assert response.status_code in (400, 409, 422)


# ============================================================================
# PATCH /program
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantUpdateProgram:
    """Tests for PATCH /api/v1/merchants/loyalty/program."""

    def test_update_program_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Update program fields."""
        response = client.patch(
            f"{BASE}/program",
            json={
                "points_per_euro": 20,
                "card_name": "Updated Rewards",
            },
            headers=loyalty_merchant_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert data["points_per_euro"] == 20
        assert data["card_name"] == "Updated Rewards"

    def test_update_program_partial(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Partial update only modifies specified fields."""
        response = client.patch(
            f"{BASE}/program",
            json={"card_name": "New Name Only"},
            headers=loyalty_merchant_headers,
        )
        assert response.status_code == 200
        data = response.json()
        assert data["card_name"] == "New Name Only"
        # Other fields should be unchanged
        assert data["points_per_euro"] == 10

    def test_update_program_deactivate(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Can deactivate program via update."""
        response = client.patch(
            f"{BASE}/program",
            json={"is_active": False},
            headers=loyalty_merchant_headers,
        )
        assert response.status_code == 200
        assert response.json()["is_active"] is False

    def test_update_program_not_found(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Update returns 404 when no program exists."""
        response = client.patch(
            f"{BASE}/program",
            json={"card_name": "No Program"},
            headers=loyalty_merchant_headers_no_program,
        )
        assert response.status_code == 404


# ============================================================================
# DELETE /program
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantDeleteProgram:
    """Tests for DELETE /api/v1/merchants/loyalty/program."""

    def test_delete_program_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup, db
    ):
        """Soft-delete the merchant's loyalty program."""
        program_id = loyalty_store_setup["program"].id

        response = client.delete(
            f"{BASE}/program", headers=loyalty_merchant_headers
        )
        assert response.status_code == 204

        # Verify soft-deleted (hidden from normal queries)
        from app.modules.loyalty.models import LoyaltyProgram

        db.expire_all()
        deleted = db.query(LoyaltyProgram).filter(LoyaltyProgram.id == program_id).first()
        assert deleted is None

        # But visible with include_deleted
        visible = (
            db.query(LoyaltyProgram)
            .execution_options(include_deleted=True)
            .filter(LoyaltyProgram.id == program_id)
            .first()
        )
        assert visible is not None
        assert visible.deleted_at is not None

    def test_delete_program_not_found(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Delete returns 404 when no program exists."""
        response = client.delete(
            f"{BASE}/program", headers=loyalty_merchant_headers_no_program
        )
        assert response.status_code == 404

    def test_delete_then_get_returns_404(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """After deletion, GET returns 404."""
        client.delete(f"{BASE}/program", headers=loyalty_merchant_headers)
        response = client.get(
            f"{BASE}/program", headers=loyalty_merchant_headers
        )
        assert response.status_code == 404


# ============================================================================
# GET /stats
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantGetStats:
    """Tests for GET /api/v1/merchants/loyalty/stats."""

    def test_get_stats_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns merchant-wide loyalty statistics."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers
        )
        assert response.status_code == 200
        data = response.json()
        assert data["merchant_id"] == loyalty_store_setup["merchant"].id
        assert data["program_id"] == loyalty_store_setup["program"].id

    def test_get_stats_includes_card_counts(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Stats include card counts from existing setup."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers
        )
        data = response.json()
        # loyalty_store_setup creates 1 card
        assert data["total_cards"] == 1
        assert data["active_cards"] == 1

    def test_get_stats_includes_all_fields(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Response includes all expected stat fields."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers
        )
        data = response.json()
        expected_fields = [
            "merchant_id",
            "program_id",
            "total_cards",
            "active_cards",
            "new_this_month",
            "total_points_issued",
            "total_points_redeemed",
            "points_issued_30d",
            "points_redeemed_30d",
            "transactions_30d",
            "estimated_liability_cents",
            "program",
            "locations",
        ]
        for field in expected_fields:
            assert field in data, f"Missing field: {field}"

    def test_get_stats_includes_program_info(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Stats include program configuration details."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers
        )
        data = response.json()
        assert data["program"] is not None
        assert data["program"]["loyalty_type"] == "points"
        assert data["program"]["points_per_euro"] == 10

    def test_get_stats_includes_locations(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Stats include per-location breakdown."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers
        )
        data = response.json()
        assert isinstance(data["locations"], list)
        # loyalty_store_setup creates 1 store
        assert len(data["locations"]) >= 1
        loc = data["locations"][0]
        assert "store_id" in loc
        assert "store_name" in loc
        assert "enrolled_count" in loc
        assert "points_earned" in loc
        assert "points_redeemed" in loc
        assert "transactions_30d" in loc

    def test_get_stats_no_program(
        self, client, loyalty_merchant_headers_no_program
    ):
        """Returns empty stats when merchant has no program."""
        response = client.get(
            f"{BASE}/stats", headers=loyalty_merchant_headers_no_program
        )
        assert response.status_code == 200
        data = response.json()
        assert data["program_id"] is None
        assert data["total_cards"] == 0
        assert data["program"] is None
        assert data["locations"] == []


# ============================================================================
# GET /cards
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantListCards:
    """Tests for GET /api/v1/merchants/loyalty/cards."""

    def test_list_cards_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns cards list for the merchant."""
        response = client.get(f"{BASE}/cards", headers=loyalty_merchant_headers)
        assert response.status_code == 200
        data = response.json()
        assert "cards" in data
        assert "total" in data
        assert isinstance(data["cards"], list)

    def test_list_cards_requires_auth(self, client):
        """Unauthenticated request is rejected."""
        response = client.get(f"{BASE}/cards")
        assert response.status_code in [401, 403]


# ============================================================================
# GET /transactions
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantListTransactions:
    """Tests for GET /api/v1/merchants/loyalty/transactions."""

    def test_list_transactions_empty(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns empty list when no transactions exist."""
        response = client.get(f"{BASE}/transactions", headers=loyalty_merchant_headers)
        assert response.status_code == 200
        data = response.json()
        assert data["transactions"] == []
        assert data["total"] == 0

    def test_list_transactions_requires_auth(self, client):
        """Unauthenticated request is rejected."""
        response = client.get(f"{BASE}/transactions")
        assert response.status_code in [401, 403]


# ============================================================================
# GET /pins
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantListPins:
    """Tests for GET /api/v1/merchants/loyalty/pins."""

    def test_list_pins_empty(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns empty list when no PINs exist."""
        response = client.get(f"{BASE}/pins", headers=loyalty_merchant_headers)
        assert response.status_code == 200
        data = response.json()
        assert data["pins"] == []
        assert data["total"] == 0

    def test_list_pins_requires_auth(self, client):
        """Unauthenticated request is rejected."""
        response = client.get(f"{BASE}/pins")
        assert response.status_code in [401, 403]


# ============================================================================
# GET /settings
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantGetSettings:
    """Tests for GET /api/v1/merchants/loyalty/settings."""

    def test_get_settings_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns merchant loyalty settings."""
        response = client.get(f"{BASE}/settings", headers=loyalty_merchant_headers)
        assert response.status_code == 200
        data = response.json()
        assert "staff_pin_policy" in data

    def test_get_settings_requires_auth(self, client):
        """Unauthenticated request is rejected."""
        response = client.get(f"{BASE}/settings")
        assert response.status_code in [401, 403]


# ============================================================================
# GET /locations
# ============================================================================


@pytest.mark.integration
@pytest.mark.api
@pytest.mark.loyalty
class TestMerchantGetLocations:
    """Tests for GET /api/v1/merchants/loyalty/locations."""

    def test_get_locations_success(
        self, client, loyalty_merchant_headers, loyalty_store_setup
    ):
        """Returns merchant's store locations."""
        response = client.get(f"{BASE}/locations", headers=loyalty_merchant_headers)
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        assert len(data) >= 1

    def test_get_locations_requires_auth(self, client):
        """Unauthenticated request is rejected."""
        response = client.get(f"{BASE}/locations")
        assert response.status_code in [401, 403]
