from __future__ import annotations

import os
from collections.abc import Iterator
from contextlib import contextmanager
from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock

import pytest
from fastapi.testclient import TestClient

os.environ.setdefault("NEON_DATABASE_URL", "postgresql://test:test@localhost/test")
os.environ.setdefault("REDIS_URL", "redis://localhost:6379/0")

from app.auth.better_auth import CurrentUser, get_current_user
from app.auth.tenant import get_current_restaurant, get_tenant_session
from app.db.session import get_session
from app.main import app
from app.models.auth_mirror import Team
from app.models.restaurant import Restaurant


class _NoopAsyncClient:
    async def aclose(self) -> None:
        return None


def _scalar_result(value: Any) -> MagicMock:
    result = MagicMock()
    result.scalar_one_or_none.return_value = value
    return result


def _slug_result(value: Restaurant | None) -> MagicMock:
    result = MagicMock()
    scalars = MagicMock()
    scalars.first.return_value = value
    result.scalars.return_value = scalars
    return result


def _make_user(*, role: str = "owner", active_org_id: str = "org-1") -> CurrentUser:
    return CurrentUser(
        id="user-1",
        email="owner@example.com",
        email_verified=True,
        active_org_id=active_org_id,
        active_team_id="team-1",
        active_restaurant_id="rest-1",
        role=role,
    )


def _make_team(*, team_id: str = "team-1", organization_id: str = "org-1") -> Team:
    now = datetime.now(UTC)
    return Team(
        id=team_id,
        name="Team One",
        organization_id=organization_id,
        created_at=now,
        updated_at=now,
    )


def _make_restaurant(*, restaurant_id: str = "rest-1", team_id: str = "team-1") -> Restaurant:
    return Restaurant(
        id=restaurant_id,
        name="Pasta Palace",
        slug="pasta-palace",
        phone="+32123456",
        team_id=team_id,
        settings={"email": "owner@example.com"},
    )


@contextmanager
def _client(
    mock_session: AsyncMock,
    monkeypatch: pytest.MonkeyPatch,
    *,
    current_user: CurrentUser | None = None,
    current_restaurant: Restaurant | None = None,
) -> Iterator[TestClient]:
    user = current_user or _make_user()
    restaurant = current_restaurant or _make_restaurant()

    async def override_get_current_user() -> CurrentUser:
        return user

    async def override_get_session() -> AsyncMock:
        return mock_session

    async def override_get_current_restaurant() -> Restaurant:
        return restaurant

    async def override_get_tenant_session() -> AsyncMock:
        return mock_session

    async def _noop_register_cron_jobs(_client: Any) -> None:
        return None

    async def _noop_emit_startup_dependency_diagnostics(_client: Any) -> None:
        return None

    async def _noop_start_subscriber() -> None:
        return None

    monkeypatch.setattr(
        "app.cron_registration.register_cron_jobs",
        _noop_register_cron_jobs,
    )
    monkeypatch.setattr(
        "app.main._emit_startup_dependency_diagnostics",
        _noop_emit_startup_dependency_diagnostics,
    )
    monkeypatch.setattr("app.realtime.pubsub.start_subscriber", _noop_start_subscriber)
    monkeypatch.setattr("app.main.httpx.AsyncClient", lambda timeout=30.0: _NoopAsyncClient())

    app.dependency_overrides[get_current_user] = override_get_current_user
    app.dependency_overrides[get_session] = override_get_session
    app.dependency_overrides[get_current_restaurant] = override_get_current_restaurant
    app.dependency_overrides[get_tenant_session] = override_get_tenant_session

    try:
        with TestClient(app) as client:
            yield client
    finally:
        app.dependency_overrides.clear()


def test_create_restaurant_requires_team_id(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    with _client(mock_session, monkeypatch) as client:
        response = client.post(
            "/api/v1/restaurants/",
            json={
                "name": "Pasta Palace",
                "address": "Main Street 1",
                "phone": "+32123456",
                "email": "owner@example.com",
                "business_type": "italian",
            },
        )

    assert response.status_code == 422
    assert any(error["loc"][-1] == "team_id" for error in response.json()["detail"])


def test_create_restaurant_rejects_missing_team(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    mock_session.execute.side_effect = [_scalar_result(None)]

    with _client(mock_session, monkeypatch) as client:
        response = client.post(
            "/api/v1/restaurants/",
            json={
                "name": "Pasta Palace",
                "address": "Main Street 1",
                "phone": "+32123456",
                "email": "owner@example.com",
                "business_type": "italian",
                "team_id": "team-1",
            },
        )

    assert response.status_code == 400
    assert response.json() == {"detail": "Team not found"}
    mock_session.add.assert_not_called()


def test_create_restaurant_rejects_team_from_other_org(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    mock_session.execute.side_effect = [_scalar_result(_make_team(organization_id="org-2"))]

    with _client(mock_session, monkeypatch) as client:
        response = client.post(
            "/api/v1/restaurants/",
            json={
                "name": "Pasta Palace",
                "address": "Main Street 1",
                "phone": "+32123456",
                "email": "owner@example.com",
                "business_type": "italian",
                "team_id": "team-1",
            },
        )

    assert response.status_code == 403
    assert response.json() == {"detail": "Team does not belong to active organization"}
    mock_session.add.assert_not_called()


def test_create_restaurant_creates_restaurant_for_valid_team(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    team = _make_team()
    mock_session.execute.side_effect = [
        _scalar_result(team),
        _scalar_result(None),
        _slug_result(None),
    ]

    with _client(mock_session, monkeypatch) as client:
        response = client.post(
            "/api/v1/restaurants/",
            json={
                "name": "Pasta Palace",
                "address": "Main Street 1",
                "phone": "+32123456",
                "email": "owner@example.com",
                "business_type": "italian",
                "team_id": "team-1",
            },
        )

    assert response.status_code == 201
    body = response.json()
    assert body["team_id"] == "team-1"
    assert body["slug"] == "pasta-palace"
    added_restaurant = mock_session.add.call_args.args[0]
    assert isinstance(added_restaurant, Restaurant)
    assert added_restaurant.team_id == "team-1"
    assert added_restaurant.settings == {
        "email": "owner@example.com",
        "address_street": "Main Street 1",
        "business_type": "italian",
    }
    mock_session.commit.assert_awaited_once()
    mock_session.refresh.assert_awaited_once_with(added_restaurant)


def test_create_restaurant_rejects_duplicate_team_use(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    mock_session.execute.side_effect = [
        _scalar_result(_make_team()),
        _scalar_result(_make_restaurant(restaurant_id="rest-existing")),
    ]

    with _client(mock_session, monkeypatch) as client:
        response = client.post(
            "/api/v1/restaurants/",
            json={
                "name": "Pasta Palace",
                "address": "Main Street 1",
                "phone": "+32123456",
                "email": "owner@example.com",
                "business_type": "italian",
                "team_id": "team-1",
            },
        )

    assert response.status_code == 409
    assert response.json() == {"detail": "Restaurant already exists for this team"}
    mock_session.add.assert_not_called()


def test_delete_restaurant_logs_team_cleanup_request(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    restaurant = _make_restaurant()
    logfire_info = MagicMock()
    monkeypatch.setattr("app.routers.restaurants.logfire.info", logfire_info)

    with _client(mock_session, monkeypatch, current_restaurant=restaurant) as client:
        response = client.delete(f"/api/v1/restaurants/{restaurant.id}")

    assert response.status_code == 204
    mock_session.delete.assert_awaited_once_with(restaurant)
    mock_session.commit.assert_awaited_once()
    assert logfire_info.mock_calls[1].args == ("restaurant_deleted",)
    assert logfire_info.mock_calls[1].kwargs == {
        "restaurant_id": restaurant.id,
        "team_id": restaurant.team_id,
        "requested_team_cleanup": True,
    }


def test_delete_restaurant_rejects_non_admin_non_owner(
    mock_session: AsyncMock, monkeypatch: pytest.MonkeyPatch
) -> None:
    restaurant = _make_restaurant()

    with _client(
        mock_session,
        monkeypatch,
        current_user=_make_user(role="member"),
        current_restaurant=restaurant,
    ) as client:
        response = client.delete(f"/api/v1/restaurants/{restaurant.id}")

    assert response.status_code == 403
    assert response.json() == {"detail": "Only owners and admins may delete"}
    mock_session.delete.assert_not_awaited()
