"""
QR Code System (INDL-11) tests.

Covers:
  - Schema validation for GenerateQRRequest and RegenerateAllRequest
  - QRCodeService.build_content_url (static, no DB)
  - QRCodeService.get_list with a mocked async DB session
  - API endpoint auth/role enforcement via AsyncClient + real test DB
"""

from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
import pytest_asyncio
from httpx import AsyncClient
from pydantic import ValidationError

# ---------------------------------------------------------------------------
# 1. Schema validation tests (synchronous — no DB needed)
# ---------------------------------------------------------------------------

class TestGenerateQRRequestSchema:
    """Pydantic validation for GenerateQRRequest."""

    def test_valid_type_entrance(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="entrance", reference_id="main-gate")
        assert req.qr_type == "entrance"

    def test_valid_type_section(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="section", reference_id="A")
        assert req.qr_type == "section"

    def test_valid_type_plot(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="plot", reference_id="A-001")
        assert req.qr_type == "plot"

    def test_valid_type_contract(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="contract", reference_id="CNT-001")
        assert req.qr_type == "contract"

    def test_invalid_type_raises(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        with pytest.raises(ValidationError):
            GenerateQRRequest(qr_type="qrcode", reference_id="X")

    def test_reference_id_too_long_raises(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        with pytest.raises(ValidationError):
            GenerateQRRequest(qr_type="plot", reference_id="A" * 101)

    def test_reference_id_empty_raises(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        with pytest.raises(ValidationError):
            GenerateQRRequest(qr_type="section", reference_id="")

    def test_reference_id_exactly_100_chars_passes(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="contract", reference_id="X" * 100)
        assert len(req.reference_id) == 100

    def test_display_label_optional(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(qr_type="entrance", reference_id="gate")
        assert req.display_label is None

    def test_display_label_accepted(self):
        from src.apps.settings.schemas.requests import GenerateQRRequest
        req = GenerateQRRequest(
            qr_type="entrance", reference_id="gate", display_label="Front Gate"
        )
        assert req.display_label == "Front Gate"


class TestRegenerateAllRequestSchema:
    """Pydantic validation for RegenerateAllRequest."""

    def test_valid_type_plot(self):
        from src.apps.settings.schemas.requests import RegenerateAllRequest
        req = RegenerateAllRequest(qr_type="plot")
        assert req.qr_type == "plot"

    def test_none_type_passes(self):
        from src.apps.settings.schemas.requests import RegenerateAllRequest
        req = RegenerateAllRequest(qr_type=None)
        assert req.qr_type is None

    def test_omitted_type_defaults_to_none(self):
        from src.apps.settings.schemas.requests import RegenerateAllRequest
        req = RegenerateAllRequest()
        assert req.qr_type is None

    def test_invalid_type_raises(self):
        from src.apps.settings.schemas.requests import RegenerateAllRequest
        with pytest.raises(ValidationError):
            RegenerateAllRequest(qr_type="badge")


# ---------------------------------------------------------------------------
# 2. QRCodeService.build_content_url tests (static — no DB)
# ---------------------------------------------------------------------------

class TestBuildContentUrl:
    """Unit tests for the pure static method build_content_url."""

    def _url(self, qr_type: str, reference_id: str = "REF") -> str:
        from src.apps.settings.services.qr_code_service import QRCodeService
        return QRCodeService.build_content_url("riverview", qr_type, reference_id)

    def test_entrance_returns_root(self):
        from src.core.config import settings
        url = self._url("entrance", "main-gate")
        assert url == f"https://riverview.{settings.APP_DOMAIN}/"

    def test_section_contains_find_section_param(self):
        from src.core.config import settings
        url = self._url("section", "B")
        assert url == f"https://riverview.{settings.APP_DOMAIN}/find?section=B"

    def test_plot_contains_find_plot_param(self):
        from src.core.config import settings
        url = self._url("plot", "B-042")
        assert url == f"https://riverview.{settings.APP_DOMAIN}/find?plot=B-042"

    def test_contract_contains_contract_path(self):
        from src.core.config import settings
        url = self._url("contract", "CNT-0001")
        assert url == f"https://riverview.{settings.APP_DOMAIN}/contract/CNT-0001"

    def test_unknown_type_falls_back_to_root(self):
        from src.core.config import settings
        from src.apps.settings.services.qr_code_service import QRCodeService
        url = QRCodeService.build_content_url("riverview", "unknown_type", "X")
        assert url == f"https://riverview.{settings.APP_DOMAIN}/"

    def test_url_encodes_space_in_section(self):
        url = self._url("section", "Section A")
        assert "Section%20A" in url
        assert " " not in url

    def test_url_encodes_ampersand_in_plot(self):
        url = self._url("plot", "A&B")
        assert "A%26B" in url
        assert "&" not in url.split("?")[1].split("=")[1]

    def test_subdomain_embedded_in_base(self):
        from src.apps.settings.services.qr_code_service import QRCodeService
        url = QRCodeService.build_content_url("greenhill", "section", "A")
        assert url.startswith("https://greenhill.")


# ---------------------------------------------------------------------------
# 3. QRCodeService.get_list tests (async — mocked DB session)
# ---------------------------------------------------------------------------

class TestGetList:
    """Service-layer tests using AsyncMock to avoid hitting the real DB."""

    def _make_mock_db(self, count: int, rows: list) -> AsyncMock:
        """Return a mock AsyncSession whose execute() yields a count then rows."""
        db = AsyncMock()

        # We need two consecutive execute() calls:
        #   1st call → scalar_one() returns `count`
        #   2nd call → scalars().all() returns `rows`
        count_result = MagicMock()
        count_result.scalar_one.return_value = count

        rows_result = MagicMock()
        rows_result.scalars.return_value.all.return_value = rows

        db.execute = AsyncMock(side_effect=[count_result, rows_result])
        return db

    @pytest.mark.asyncio
    async def test_get_list_empty(self):
        from src.apps.settings.services.qr_code_service import QRCodeService

        db = self._make_mock_db(count=0, rows=[])
        tenant_id = str(uuid4())

        items, total = await QRCodeService.get_list(db, tenant_id)

        assert total == 0
        assert items == []
        assert db.execute.call_count == 2

    @pytest.mark.asyncio
    async def test_get_list_returns_all_items(self):
        from src.apps.settings.services.qr_code_service import QRCodeService

        fake_qr = MagicMock()
        fake_qr.qr_type = "plot"
        db = self._make_mock_db(count=1, rows=[fake_qr])
        tenant_id = str(uuid4())

        items, total = await QRCodeService.get_list(db, tenant_id)

        assert total == 1
        assert len(items) == 1
        assert items[0].qr_type == "plot"

    @pytest.mark.asyncio
    async def test_get_list_filters_by_type(self):
        """When qr_type is provided, the WHERE clause should include it.

        We verify this by checking that execute() is still called twice
        (count + rows) and that the mock correctly reflects the filtered result.
        """
        from src.apps.settings.services.qr_code_service import QRCodeService

        fake_qr = MagicMock()
        fake_qr.qr_type = "section"
        db = self._make_mock_db(count=1, rows=[fake_qr])
        tenant_id = str(uuid4())

        items, total = await QRCodeService.get_list(db, tenant_id, qr_type="section")

        assert total == 1
        assert items[0].qr_type == "section"
        # Both the COUNT and the SELECT should have been executed
        assert db.execute.call_count == 2

    @pytest.mark.asyncio
    async def test_get_list_pagination_offset(self):
        """page=2 with page_size=5 should produce the second page."""
        from src.apps.settings.services.qr_code_service import QRCodeService

        db = self._make_mock_db(count=10, rows=[])
        tenant_id = str(uuid4())

        items, total = await QRCodeService.get_list(db, tenant_id, page=2, page_size=5)

        assert total == 10
        assert items == []

    @pytest.mark.asyncio
    async def test_get_list_no_type_filter(self):
        """Calling without qr_type should not add a type filter (no error)."""
        from src.apps.settings.services.qr_code_service import QRCodeService

        db = self._make_mock_db(count=3, rows=[MagicMock(), MagicMock(), MagicMock()])
        tenant_id = str(uuid4())

        items, total = await QRCodeService.get_list(db, tenant_id, qr_type=None)

        assert total == 3
        assert len(items) == 3


# ---------------------------------------------------------------------------
# 4. API endpoint tests (real DB via AsyncClient)
# ---------------------------------------------------------------------------

@pytest_asyncio.fixture
async def qr_tenant(db_session):
    """Isolated tenant account for QR code endpoint tests."""
    from src.apps.tenants.models.account import Account

    account = Account(
        organization_name="QR Test Cemetery",
        subdomain=f"qr-test-{uuid4().hex[:8]}",
        contact_email=f"qr-{uuid4().hex[:6]}@qrtest.com",
        plan="starter",
        status="active",
    )
    db_session.add(account)
    await db_session.flush()
    return account


@pytest_asyncio.fixture
async def qr_staff_user(db_session, qr_tenant):
    from src.apps.auth.models.user import User
    from src.core.security import hash_password

    user = User(
        tenant_id=qr_tenant.id,
        email=f"staff-{uuid4().hex[:6]}@qrtest.com",
        password_hash=hash_password("TestPassword123!"),
        first_name="Staff",
        last_name="QR",
        role="staff",
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


@pytest_asyncio.fixture
async def qr_admin_user(db_session, qr_tenant):
    from src.apps.auth.models.user import User
    from src.core.security import hash_password

    user = User(
        tenant_id=qr_tenant.id,
        email=f"admin-{uuid4().hex[:6]}@qrtest.com",
        password_hash=hash_password("TestPassword123!"),
        first_name="Admin",
        last_name="QR",
        role="administrator",
        status="active",
    )
    db_session.add(user)
    await db_session.flush()
    return user


@pytest_asyncio.fixture
async def qr_staff_headers(qr_staff_user, qr_tenant):
    from src.core.security import create_access_token, build_token_payload

    token = create_access_token(build_token_payload(qr_staff_user, qr_tenant))
    return {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": str(qr_tenant.id),
    }


@pytest_asyncio.fixture
async def qr_admin_headers(qr_admin_user, qr_tenant):
    from src.core.security import create_access_token, build_token_payload

    token = create_access_token(build_token_payload(qr_admin_user, qr_tenant))
    return {
        "Authorization": f"Bearer {token}",
        "X-Tenant-ID": str(qr_tenant.id),
    }


# 4a. List endpoint

@pytest.mark.asyncio
async def test_list_qr_codes_requires_auth(client: AsyncClient, qr_tenant):
    """GET /qr-codes without a token must return 401."""
    resp = await client.get(
        "/api/v1/settings/qr-codes",
        headers={"X-Tenant-ID": str(qr_tenant.id)},
    )
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_list_qr_codes_staff_can_access(client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant):
    """Staff role (minimum allowed) should get 200 on the list endpoint."""
    resp = await client.get("/api/v1/settings/qr-codes", headers=qr_staff_headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["success"] is True


@pytest.mark.asyncio
async def test_list_qr_codes_empty_for_new_tenant(client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant):
    """A brand-new tenant has no QR codes — list should return total=0."""
    resp = await client.get("/api/v1/settings/qr-codes", headers=qr_staff_headers)
    assert resp.status_code == 200
    body = resp.json()
    assert body["total"] == 0
    assert body["data"] == []


# 4b. Generate endpoint

@pytest.mark.asyncio
async def test_generate_endpoint_requires_auth(client: AsyncClient, qr_tenant):
    """POST /qr-codes/generate without a token must return 401."""
    resp = await client.post(
        "/api/v1/settings/qr-codes/generate",
        json={"qr_type": "entrance", "reference_id": "gate"},
        headers={"X-Tenant-ID": str(qr_tenant.id)},
    )
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_generate_endpoint_invalid_schema_returns_422(
    client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant
):
    """Sending an invalid qr_type must return 422 Unprocessable Entity."""
    resp = await client.post(
        "/api/v1/settings/qr-codes/generate",
        json={"qr_type": "invalid_type", "reference_id": "x"},
        headers=qr_staff_headers,
    )
    assert resp.status_code == 422


@pytest.mark.asyncio
async def test_generate_entrance_enqueues_job(
    client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant
):
    """Generating an entrance QR code should return 202 with a qr_code_id.

    Redis/ARQ is mocked to avoid a real connection requirement in the test suite.
    """
    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_arq = AsyncMock()
        mock_arq.enqueue_job = AsyncMock(return_value=MagicMock(job_id="test-job-id"))
        mock_arq.aclose = AsyncMock()
        mock_pool.return_value = mock_arq

        resp = await client.post(
            "/api/v1/settings/qr-codes/generate",
            json={"qr_type": "entrance", "reference_id": "main-gate"},
            headers=qr_staff_headers,
        )

    assert resp.status_code == 202
    body = resp.json()
    assert body["success"] is True
    assert "qr_code_id" in body["data"]


# 4c. Regenerate-all endpoint

@pytest.mark.asyncio
async def test_regenerate_all_requires_auth(client: AsyncClient, qr_tenant):
    """POST /qr-codes/regenerate-all without a token must return 401."""
    resp = await client.post(
        "/api/v1/settings/qr-codes/regenerate-all",
        json={},
        headers={"X-Tenant-ID": str(qr_tenant.id)},
    )
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_regenerate_all_requires_administrator(
    client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant
):
    """Staff role is below ADMINISTRATOR — must receive 403 on regenerate-all."""
    resp = await client.post(
        "/api/v1/settings/qr-codes/regenerate-all",
        json={},
        headers=qr_staff_headers,
    )
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_regenerate_all_admin_succeeds(
    client: AsyncClient, qr_admin_headers, qr_admin_user, qr_tenant
):
    """Administrator role can call regenerate-all; returns 202 with enqueued count."""
    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_arq = AsyncMock()
        mock_arq.enqueue_job = AsyncMock(return_value=MagicMock(job_id="job-abc"))
        mock_arq.aclose = AsyncMock()
        mock_pool.return_value = mock_arq

        resp = await client.post(
            "/api/v1/settings/qr-codes/regenerate-all",
            json={},
            headers=qr_admin_headers,
        )

    assert resp.status_code == 202
    body = resp.json()
    assert body["success"] is True
    assert "enqueued" in body["data"]


@pytest.mark.asyncio
async def test_regenerate_all_with_type_filter(
    client: AsyncClient, qr_admin_headers, qr_admin_user, qr_tenant
):
    """Passing qr_type to regenerate-all should be accepted (valid Literal value)."""
    with patch("arq.create_pool", new_callable=AsyncMock) as mock_pool:
        mock_arq = AsyncMock()
        mock_arq.enqueue_job = AsyncMock()
        mock_arq.aclose = AsyncMock()
        mock_pool.return_value = mock_arq

        resp = await client.post(
            "/api/v1/settings/qr-codes/regenerate-all",
            json={"qr_type": "plot"},
            headers=qr_admin_headers,
        )

    assert resp.status_code == 202


@pytest.mark.asyncio
async def test_regenerate_all_invalid_type_returns_422(
    client: AsyncClient, qr_admin_headers, qr_admin_user, qr_tenant
):
    """Sending an invalid qr_type to regenerate-all must return 422."""
    resp = await client.post(
        "/api/v1/settings/qr-codes/regenerate-all",
        json={"qr_type": "badge"},
        headers=qr_admin_headers,
    )
    assert resp.status_code == 422


# 4d. Download endpoint

@pytest.mark.asyncio
async def test_download_requires_auth(client: AsyncClient, qr_tenant):
    """GET /qr-codes/{id}/download without a token must return 401."""
    resp = await client.get(
        f"/api/v1/settings/qr-codes/{uuid4()}/download",
        headers={"X-Tenant-ID": str(qr_tenant.id)},
    )
    assert resp.status_code == 401


@pytest.mark.asyncio
async def test_download_nonexistent_qr_returns_404(
    client: AsyncClient, qr_staff_headers, qr_staff_user, qr_tenant
):
    """Requesting a download URL for a non-existent QR code UUID must return 404."""
    resp = await client.get(
        f"/api/v1/settings/qr-codes/{uuid4()}/download",
        headers=qr_staff_headers,
    )
    assert resp.status_code == 404
