"""Tests for the channel gateway HTTP notify endpoint."""

from __future__ import annotations

from unittest.mock import AsyncMock

import pytest
from starlette.testclient import TestClient

from turnstone.channels._http import create_channel_app
from turnstone.core.auth import JWT_AUD_CHANNEL, create_jwt
from turnstone.core.storage._sqlite import SQLiteBackend

_JWT_SECRET = "a" * 32


def _make_jwt() -> str:
    """Create a valid JWT for channel auth."""
    return create_jwt(
        user_id="system",
        scopes=frozenset({"write"}),
        source="service",
        secret=_JWT_SECRET,
        audience=JWT_AUD_CHANNEL,
    )


def _auth_headers() -> dict[str, str]:
    return {"Authorization": f"Bearer {_make_jwt()}"}


@pytest.fixture
def storage(tmp_path):
    return SQLiteBackend(str(tmp_path / "test.db"))


@pytest.fixture
def mock_adapter():
    adapter = AsyncMock()
    adapter.channel_type = "discord"
    adapter.send = AsyncMock(return_value="msg_001")
    return adapter


@pytest.fixture
def no_auth_client(storage, mock_adapter):
    """Client with no auth configured (for fail-closed tests)."""
    app = create_channel_app({"discord": mock_adapter}, storage)
    return TestClient(app)


@pytest.fixture
def client(storage, mock_adapter):
    """Default client with JWT auth configured."""
    app = create_channel_app({"discord": mock_adapter}, storage, jwt_secret=_JWT_SECRET)
    return TestClient(app)


@pytest.fixture
def authed_client(storage, mock_adapter):
    """Alias -- same as client, for auth-specific test clarity."""
    app = create_channel_app({"discord": mock_adapter}, storage, jwt_secret=_JWT_SECRET)
    return TestClient(app)


@pytest.fixture
def jwt_client(storage, mock_adapter):
    """Client with JWT auth configured."""
    app = create_channel_app({"discord": mock_adapter}, storage, jwt_secret=_JWT_SECRET)
    return TestClient(app)


class TestNotifyEndpoint:
    def test_health(self, client):
        resp = client.get("/health")
        assert resp.status_code == 200
        assert resp.json()["status"] == "ok"

    def test_direct_discord_target(self, client, mock_adapter):
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123456"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        assert len(results) == 1
        assert results[0]["status"] == "sent"
        assert results[0]["message_id"] == "msg_001"
        mock_adapter.send.assert_called_once_with("123456", "Hello!")

    def test_with_title(self, client, mock_adapter):
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123456"},
                "message": "Hello!",
                "title": "Alert",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        mock_adapter.send.assert_called_once_with("123456", "**Alert**\nHello!")

    def test_username_resolution(self, client, storage, mock_adapter):
        # Create a user and link a channel
        storage.create_user("u1", "testuser", "Test User", "hash")
        storage.create_channel_user("discord", "disc_123", "u1")

        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"username": "testuser"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        assert len(results) == 1
        assert results[0]["status"] == "sent"
        mock_adapter.send.assert_called_once_with("disc_123", "Hello!")

    def test_unknown_username(self, client):
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"username": "nobody"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 404
        error = resp.json()["error"]
        assert "nobody" not in error
        assert "not found or has no linked channels" in error

    def test_user_no_channels(self, authed_client, storage):
        storage.create_user("u1", "testuser", "Test User", "hash")

        resp = authed_client.post(
            "/v1/api/notify",
            json={
                "target": {"username": "testuser"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 404
        # Generic message -- must not differentiate "not found" vs "no channels"
        error = resp.json()["error"]
        assert "testuser" not in error
        assert "not found or has no linked channels" in error

    def test_missing_fields(self, client):
        resp = client.post(
            "/v1/api/notify",
            json={"target": {"username": "x"}},
            headers=_auth_headers(),
        )
        assert resp.status_code == 400

    def test_missing_target(self, client):
        resp = client.post(
            "/v1/api/notify",
            json={"message": "Hello!"},
            headers=_auth_headers(),
        )
        assert resp.status_code == 400

    def test_invalid_target(self, client):
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"invalid": "field"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 400

    def test_no_adapter(self, client, storage):
        # App has discord adapter, try email target
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "email", "channel_id": "test@example.com"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        assert results[0]["status"] == "no_adapter"

    def test_adapter_failure(self, client, mock_adapter):
        mock_adapter.send.side_effect = RuntimeError("Discord API error")
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123456"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        assert results[0]["status"] == "failed"

    def test_adapter_timeout(self, storage, mock_adapter, monkeypatch):
        """Adapter calls that exceed the timeout return timeout status."""
        import asyncio

        async def _hang(*_args: object) -> str:
            await asyncio.sleep(300)
            return ""

        mock_adapter.send = _hang

        # Use a very short timeout to keep the test fast
        from turnstone.channels import _http as _http_mod

        monkeypatch.setattr(_http_mod, "_NOTIFY_ADAPTER_TIMEOUT", 0.1)
        app = create_channel_app({"discord": mock_adapter}, storage, jwt_secret=_JWT_SECRET)
        tc = TestClient(app)
        resp = tc.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123456"},
                "message": "Hello!",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        results = resp.json()["results"]
        assert results[0]["status"] == "timeout"

    def test_invalid_json(self, client):
        resp = client.post(
            "/v1/api/notify",
            content=b"not json",
            headers={
                "content-type": "application/json",
                "Authorization": f"Bearer {_make_jwt()}",
            },
        )
        assert resp.status_code == 400

    def test_whitespace_only_message(self, client):
        """Whitespace-only messages should be rejected."""
        resp = client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "   ",
            },
            headers=_auth_headers(),
        )
        assert resp.status_code == 400


class TestNotifyAuth:
    """Tests for authentication on the /v1/api/notify endpoint."""

    def test_reject_when_unconfigured(self, no_auth_client):
        """Requests are rejected (fail closed) when no auth is configured."""
        resp = no_auth_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
        )
        assert resp.status_code == 401

    def test_reject_without_token(self, authed_client):
        """Requests without Authorization header are rejected when auth is configured."""
        resp = authed_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
        )
        assert resp.status_code == 401

    def test_reject_wrong_token(self, authed_client):
        """Requests with wrong token are rejected."""
        resp = authed_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
            headers={"Authorization": "Bearer wrong-token"},
        )
        assert resp.status_code == 401

    def test_accept_valid_jwt(self, jwt_client, mock_adapter):
        """Requests with a valid JWT for the channel audience are accepted."""
        token = _make_jwt()
        resp = jwt_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
            headers={"Authorization": f"Bearer {token}"},
        )
        assert resp.status_code == 200

    def test_reject_jwt_wrong_audience(self, jwt_client):
        """JWTs with wrong audience are rejected."""
        token = create_jwt(
            user_id="system",
            scopes=frozenset({"write"}),
            source="service",
            secret=_JWT_SECRET,
            audience="turnstone-server",  # wrong audience
        )
        resp = jwt_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
            headers={"Authorization": f"Bearer {token}"},
        )
        assert resp.status_code == 401

    def test_reject_jwt_wrong_secret(self, jwt_client):
        """JWTs signed with wrong secret are rejected."""
        token = create_jwt(
            user_id="system",
            scopes=frozenset({"write"}),
            source="service",
            secret="b" * 32,  # wrong secret
            audience=JWT_AUD_CHANNEL,
        )
        resp = jwt_client.post(
            "/v1/api/notify",
            json={
                "target": {"channel_type": "discord", "channel_id": "123"},
                "message": "Hello!",
            },
            headers={"Authorization": f"Bearer {token}"},
        )
        assert resp.status_code == 401

    def test_health_bypasses_auth(self, authed_client):
        """Health endpoint is always accessible regardless of auth config."""
        resp = authed_client.get("/health")
        assert resp.status_code == 200
