"""
Integration and unit tests for Discount Rules & Calculation Engine.
"""

from decimal import Decimal
from uuid import uuid4

import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ValidationException
from app.core.security import create_access_token, hash_password
from app.models.audit import AuditLog
from app.models.billing import DiscountRule, DiscountType
from app.models.lab import Lab
from app.models.user import User, UserRole
from app.repositories.permission_repository import PermissionRepository
from app.services.discount_service import DiscountService


@pytest_asyncio.fixture(autouse=True)
async def seed_perms(db_session: AsyncSession) -> None:
    """Seed system permissions for RBAC enforcement."""
    repo = PermissionRepository()
    await repo.seed_permissions(db_session)


@pytest_asyncio.fixture
async def discount_lab(db_session: AsyncSession) -> Lab:
    """Create primary test branch lab."""
    lab = Lab(name="Main Discount Lab", address="Kathmandu")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def secondary_lab(db_session: AsyncSession) -> Lab:
    """Create secondary branch lab for branch isolation testing."""
    lab = Lab(name="Pokhara Branch Lab", address="Pokhara")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def admin_token(db_session: AsyncSession, discount_lab: Lab) -> str:
    """Create branch admin user token with DISCOUNT_READ and DISCOUNT_WRITE."""
    admin = User(
        lab_id=discount_lab.id,
        name="Billing Admin",
        phone="9817000001",
        role=UserRole.SUPERADMIN,
        hashed_password=hash_password("AdminPass123!"),
        is_active=True,
    )
    db_session.add(admin)
    await db_session.commit()
    await db_session.refresh(admin)

    return create_access_token(
        subject=str(admin.id),
        extra_claims={
            "phone": admin.phone,
            "role": admin.role.value,
            "lab_id": str(discount_lab.id),
        },
    )


@pytest_asyncio.fixture
async def secondary_admin_token(db_session: AsyncSession, secondary_lab: Lab) -> str:
    """Create secondary branch admin token."""
    admin = User(
        lab_id=secondary_lab.id,
        name="Pokhara Admin",
        phone="9817000002",
        role=UserRole.ACCOUNTANT,
        hashed_password=hash_password("AdminPass123!"),
        is_active=True,
    )
    db_session.add(admin)
    await db_session.commit()
    await db_session.refresh(admin)

    return create_access_token(
        subject=str(admin.id),
        extra_claims={
            "phone": admin.phone,
            "role": admin.role.value,
            "lab_id": str(secondary_lab.id),
        },
    )


@pytest_asyncio.fixture
async def general_user_token(db_session: AsyncSession, discount_lab: Lab) -> str:
    """Create general user token lacking discount write permissions."""
    user = User(
        lab_id=discount_lab.id,
        name="General Staff",
        phone="9817000003",
        role=UserRole.GENERAL_USER,
        hashed_password=hash_password("StaffPass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)

    return create_access_token(
        subject=str(user.id),
        extra_claims={
            "phone": user.phone,
            "role": user.role.value,
            "lab_id": str(discount_lab.id),
        },
    )


@pytest.mark.asyncio
class TestDiscountRules:
    """Tests covering discount rule management, validations, and branch isolation."""

    async def test_create_percentage_discount_rule(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}
        payload = {
            "name": "Senior Citizen 15%",
            "code": "SR15",
            "discount_type": "percentage",
            "discount_value": 15.00,
            "is_active": True,
            "description": "15% off for senior citizens above 60",
        }
        res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json=payload,
            headers=headers,
        )
        assert res.status_code == 201
        data = res.json()["data"]
        assert data["name"] == "Senior Citizen 15%"
        assert data["code"] == "SR15"
        assert data["discount_type"] == "percentage"
        assert float(data["discount_value"]) == 15.00
        assert data["is_active"] is True
        assert data["lab_id"] == str(discount_lab.id)

    async def test_create_fixed_discount_rule(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}
        payload = {
            "name": "Festival Flat 500",
            "code": "FEST500",
            "discount_type": "fixed",
            "discount_value": 500.00,
            "is_active": True,
        }
        res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json=payload,
            headers=headers,
        )
        assert res.status_code == 201
        data = res.json()["data"]
        assert data["name"] == "Festival Flat 500"
        assert data["discount_type"] == "fixed"
        assert float(data["discount_value"]) == 500.00

    async def test_percentage_discount_validation_errors(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}

        # 0.00% is invalid
        res1 = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={
                "name": "Zero Percentage",
                "discount_type": "percentage",
                "discount_value": 0.00,
            },
            headers=headers,
        )
        assert res1.status_code == 422

        # 105.00% is invalid
        res2 = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={
                "name": "Over 100 Percentage",
                "discount_type": "percentage",
                "discount_value": 105.00,
            },
            headers=headers,
        )
        assert res2.status_code == 422

        # Negative percentage
        res3 = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={
                "name": "Negative Percentage",
                "discount_type": "percentage",
                "discount_value": -10.00,
            },
            headers=headers,
        )
        assert res3.status_code == 422

    async def test_fixed_discount_validation_errors(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}

        # Negative fixed amount
        res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={
                "name": "Negative Fixed",
                "discount_type": "fixed",
                "discount_value": -100.00,
            },
            headers=headers,
        )
        assert res.status_code == 422

    async def test_duplicate_discount_name_in_branch_rejected(
        self,
        client: AsyncClient,
        admin_token: str,
        secondary_admin_token: str,
        discount_lab: Lab,
        secondary_lab: Lab,
    ) -> None:
        headers1 = {"Authorization": f"Bearer {admin_token}"}
        payload = {
            "name": "Hospital Staff Discount",
            "discount_type": "percentage",
            "discount_value": 20.00,
        }

        # 1. Create first rule in discount_lab
        res1 = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json=payload,
            headers=headers1,
        )
        assert res1.status_code == 201

        # 2. Duplicate in same lab branch -> 409 Conflict
        res2 = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json=payload,
            headers=headers1,
        )
        assert res2.status_code == 409

        # 3. Same name in a DIFFERENT lab branch -> Allowed (201 Created)
        headers2 = {"Authorization": f"Bearer {secondary_admin_token}"}
        res3 = await client.post(
            f"/api/v1/discounts?lab_id={secondary_lab.id}",
            json=payload,
            headers=headers2,
        )
        assert res3.status_code == 201

    async def test_list_and_search_discounts(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}

        # Create 2 rules
        await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={"name": "Alpha Special", "code": "ALPHA", "discount_type": "fixed", "discount_value": 100},
            headers=headers,
        )
        await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={
                "name": "Beta Inactive",
                "code": "BETA",
                "discount_type": "percentage",
                "discount_value": 10,
                "is_active": False,
            },
            headers=headers,
        )

        # List all
        res = await client.get(f"/api/v1/discounts?lab_id={discount_lab.id}", headers=headers)
        assert res.status_code == 200
        assert len(res.json()["data"]) >= 2

        # Filter active
        url = f"/api/v1/discounts?lab_id={discount_lab.id}&is_active=true"
        res_active = await client.get(url, headers=headers)
        assert res_active.status_code == 200
        active_names = [d["name"] for d in res_active.json()["data"]]
        assert "Alpha Special" in active_names
        assert "Beta Inactive" not in active_names

        # Search by keyword
        res_search = await client.get(f"/api/v1/discounts?lab_id={discount_lab.id}&q=ALPHA", headers=headers)
        assert res_search.status_code == 200
        assert len(res_search.json()["data"]) == 1
        assert res_search.json()["data"][0]["name"] == "Alpha Special"

    async def test_update_and_delete_discount_rule(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
        db_session: AsyncSession,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}

        # 1. Create
        create_res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={"name": "Vip Patient", "discount_type": "percentage", "discount_value": 25.00},
            headers=headers,
        )
        assert create_res.status_code == 201
        rule_id = create_res.json()["data"]["id"]

        # 2. Get by ID
        get_res = await client.get(f"/api/v1/discounts/{rule_id}", headers=headers)
        assert get_res.status_code == 200
        assert get_res.json()["data"]["name"] == "Vip Patient"

        # 3. Update
        patch_res = await client.patch(
            f"/api/v1/discounts/{rule_id}",
            json={"name": "VIP Platinum", "discount_value": 30.00},
            headers=headers,
        )
        assert patch_res.status_code == 200
        assert patch_res.json()["data"]["name"] == "VIP Platinum"
        assert float(patch_res.json()["data"]["discount_value"]) == 30.00

        audit_res = await db_session.execute(
            select(AuditLog).where(AuditLog.target_id == rule_id, AuditLog.target_type == "discount_scheme")
        )
        audit_entries = audit_res.scalars().all()
        assert {entry.action for entry in audit_entries} >= {
            "DISCOUNT_SCHEME_CREATED",
            "DISCOUNT_SCHEME_UPDATED",
        }
        assert all(entry.category == "BILLING" for entry in audit_entries)

        # 4. Soft delete
        del_res = await client.delete(f"/api/v1/discounts/{rule_id}", headers=headers)
        assert del_res.status_code == 200

        audit_res = await db_session.execute(
            select(AuditLog.action).where(
                AuditLog.target_id == rule_id, AuditLog.target_type == "discount_scheme"
            )
        )
        assert "DISCOUNT_SCHEME_REMOVED" in audit_res.scalars().all()

        # 5. Fetch after delete -> 404
        get_after = await client.get(f"/api/v1/discounts/{rule_id}", headers=headers)
        assert get_after.status_code == 404

    async def test_percentage_type_change_cannot_keep_an_over_100_fixed_value(
        self,
        client: AsyncClient,
        admin_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}
        create_res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={"name": "Large Fixed Credit", "discount_type": "fixed", "discount_value": 250},
            headers=headers,
        )
        assert create_res.status_code == 201
        rule_id = create_res.json()["data"]["id"]

        patch_res = await client.patch(
            f"/api/v1/discounts/{rule_id}",
            json={"discount_type": "percentage"},
            headers=headers,
        )
        assert patch_res.status_code == 422

    async def test_branch_isolation_on_discounts(
        self,
        client: AsyncClient,
        admin_token: str,
        secondary_admin_token: str,
        discount_lab: Lab,
        secondary_lab: Lab,
    ) -> None:
        headers_lab1 = {"Authorization": f"Bearer {admin_token}"}
        headers_lab2 = {"Authorization": f"Bearer {secondary_admin_token}"}

        # Lab 1 creates a private discount
        c_res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={"name": "Lab1 Secret Discount", "discount_type": "fixed", "discount_value": 300},
            headers=headers_lab1,
        )
        assert c_res.status_code == 201
        rule_id = c_res.json()["data"]["id"]

        # Lab 2 attempts to fetch Lab 1's discount -> 404 Not Found
        get_res = await client.get(f"/api/v1/discounts/{rule_id}", headers=headers_lab2)
        assert get_res.status_code == 404

        # Lab 2 attempts to modify Lab 1's discount -> 404 Not Found
        patch_res = await client.patch(
            f"/api/v1/discounts/{rule_id}",
            json={"name": "Hacked Name"},
            headers=headers_lab2,
        )
        assert patch_res.status_code == 404

    async def test_rbac_discount_permissions(
        self,
        client: AsyncClient,
        general_user_token: str,
        discount_lab: Lab,
    ) -> None:
        headers = {"Authorization": f"Bearer {general_user_token}"}

        # General user lacks discount:write -> 403 Forbidden
        res = await client.post(
            f"/api/v1/discounts?lab_id={discount_lab.id}",
            json={"name": "Unauthorized Discount", "discount_type": "fixed", "discount_value": 50},
            headers=headers,
        )
        assert res.status_code == 403


class TestDiscountCalculationEngine:
    """Unit tests for DiscountService.calculate_discount domain engine."""

    def test_percentage_discount_calculation(self) -> None:
        service = DiscountService()
        rule = DiscountRule(
            id=uuid4(),
            lab_id=uuid4(),
            name="10% Off",
            discount_type=DiscountType.PERCENTAGE,
            discount_value=Decimal("10.00"),
            is_active=True,
        )

        # 10% of 1000 = 100.00
        res = service.calculate_discount(Decimal("1000.00"), rule)
        assert res == Decimal("100.00")

        # 10% of 155.50 = 15.55
        res2 = service.calculate_discount(Decimal("155.50"), rule)
        assert res2 == Decimal("15.55")

    def test_fixed_discount_capped_at_subtotal(self) -> None:
        service = DiscountService()
        rule = DiscountRule(
            id=uuid4(),
            lab_id=uuid4(),
            name="Flat 500",
            discount_type=DiscountType.FIXED,
            discount_value=Decimal("500.00"),
            is_active=True,
        )

        # Subtotal 1200 -> discount 500
        res1 = service.calculate_discount(Decimal("1200.00"), rule)
        assert res1 == Decimal("500.00")

        # Subtotal 300 -> discount capped at 300 (cannot exceed subtotal)
        res2 = service.calculate_discount(Decimal("300.00"), rule)
        assert res2 == Decimal("300.00")

    def test_zero_subtotal_discount(self) -> None:
        service = DiscountService()
        rule = DiscountRule(
            id=uuid4(),
            lab_id=uuid4(),
            name="Any Rule",
            discount_type=DiscountType.PERCENTAGE,
            discount_value=Decimal("20.00"),
            is_active=True,
        )
        res = service.calculate_discount(Decimal("0.00"), rule)
        assert res == Decimal("0.00")

    def test_inactive_rule_raises_validation_exception(self) -> None:
        service = DiscountService()
        inactive_rule = DiscountRule(
            id=uuid4(),
            lab_id=uuid4(),
            name="Expired Rule",
            discount_type=DiscountType.PERCENTAGE,
            discount_value=Decimal("10.00"),
            is_active=False,
        )
        with pytest.raises(ValidationException, match="inactive and cannot be applied"):
            service.calculate_discount(Decimal("1000.00"), inactive_rule)
