"""Unit tests for macro API endpoints and dashboard components.

Tests macro event list/detail endpoints, macro toggle endpoint,
and trend projection endpoint return correct data structures.

Requirements: 8.1, 8.2, 11.5, 12.10
"""
from __future__ import annotations

import json
from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
from uuid import uuid4

import pytest
from httpx import ASGITransport, AsyncClient

from services.api.app import app

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

NOW = datetime(2026, 5, 15, 14, 0, 0, tzinfo=timezone.utc)


class FakeRecord(dict):
    """Mimics asyncpg.Record for testing."""
    def items(self):
        return super().items()


def _make_event_row(event_id: str | None = None) -> FakeRecord:
    eid = event_id or str(uuid4())
    return FakeRecord({
        "id": eid,
        "event_types": ["trade_barrier", "cost_increase"],
        "severity": "high",
        "affected_regions": ["US", "CN"],
        "affected_sectors": ["Technology"],
        "affected_commodities": ["semiconductors"],
        "summary": "US tariffs on Chinese semiconductors",
        "key_facts": json.dumps(["25% tariff", "Effective in 30 days"]),
        "estimated_duration": "medium_term",
        "confidence": 0.85,
        "source_document_id": str(uuid4()),
        "created_at": NOW,
        # Detail fields
        "model_provider": "ollama",
        "model_name": "test-model",
        "prompt_version": "event-v1",
        "schema_version": "1.0.0",
    })


def _make_impact_row(event_id: str) -> FakeRecord:
    return FakeRecord({
        "id": str(uuid4()),
        "event_id": event_id,
        "company_id": str(uuid4()),
        "ticker": "AAPL",
        "macro_impact_score": 0.45,
        "impact_direction": "negative",
        "contributing_factors": json.dumps(["geographic_overlap:0.650"]),
        "confidence": 0.8,
        "computed_at": NOW,
        "legal_name": "Apple Inc.",
        "sector": "Technology",
        # For ticker endpoint
        "event_summary": "US tariffs on Chinese semiconductors",
        "event_severity": "high",
        "event_types": ["trade_barrier"],
        "affected_regions": ["US", "CN"],
    })


def _make_projection_row(trend_id: str) -> FakeRecord:
    return FakeRecord({
        "id": str(uuid4()),
        "trend_window_id": trend_id,
        "projected_direction": "bearish",
        "projected_strength": 0.6,
        "projected_confidence": 0.5,
        "projection_horizon": "7d",
        "driving_factors": json.dumps(["Macro signals project bearish impact"]),
        "macro_contribution_pct": 0.3,
        "diverges_from_current": True,
        "computed_at": NOW,
    })


# ---------------------------------------------------------------------------
# Route structure tests
# ---------------------------------------------------------------------------


class TestMacroRouteStructure:
    """Verify all macro-related routes are registered."""

    def test_macro_event_list_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/macro/events" in paths

    def test_macro_event_detail_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/macro/events/{event_id}" in paths

    def test_macro_impacts_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/macro/impacts/{ticker}" in paths

    def test_macro_status_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/admin/macro/status" in paths

    def test_macro_toggle_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/admin/macro/toggle" in paths

    def test_trend_projection_route_exists(self):
        paths = [route.path for route in app.routes]
        assert "/api/trends/{trend_id}/projection" in paths


# ---------------------------------------------------------------------------
# Macro event endpoints (Requirements: 8.1, 8.2)
# ---------------------------------------------------------------------------


class TestMacroEventEndpoints:
    """Test macro event list and detail endpoints."""

    @pytest.mark.asyncio
    async def test_list_macro_events_returns_events(self):
        """GET /api/macro/events should return a list of events."""
        event_row = _make_event_row()
        mock_pool = AsyncMock()
        mock_pool.fetch = AsyncMock(return_value=[event_row])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get("/api/macro/events")

        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert len(data) == 1
        assert data[0]["severity"] == "high"
        assert data[0]["summary"] == "US tariffs on Chinese semiconductors"
        assert isinstance(data[0]["key_facts"], list)

    @pytest.mark.asyncio
    async def test_list_macro_events_with_severity_filter(self):
        """GET /api/macro/events?severity=high should filter by severity."""
        event_row = _make_event_row()
        mock_pool = AsyncMock()
        mock_pool.fetch = AsyncMock(return_value=[event_row])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get("/api/macro/events?severity=high")

        assert resp.status_code == 200
        # Verify the query was called (filter applied)
        mock_pool.fetch.assert_called_once()
        call_args = mock_pool.fetch.call_args
        assert "high" in call_args.args

    @pytest.mark.asyncio
    async def test_get_macro_event_detail(self):
        """GET /api/macro/events/{id} should return event with affected companies."""
        event_id = str(uuid4())
        event_row = _make_event_row(event_id)
        impact_row = _make_impact_row(event_id)

        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(return_value=event_row)
        mock_pool.fetch = AsyncMock(return_value=[impact_row])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get(f"/api/macro/events/{event_id}")

        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == event_id
        assert data["severity"] == "high"
        assert "impacts" in data
        assert len(data["impacts"]) == 1
        assert data["impacts"][0]["ticker"] == "AAPL"

    @pytest.mark.asyncio
    async def test_get_macro_event_not_found(self):
        """GET /api/macro/events/{id} should return 404 for missing event."""
        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(return_value=None)

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get(f"/api/macro/events/{uuid4()}")

        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Macro toggle endpoint (Requirement: 11.5)
# ---------------------------------------------------------------------------


class TestMacroToggleEndpoint:
    """Test macro toggle endpoint persists state and records audit event."""

    @pytest.mark.asyncio
    async def test_get_macro_status_returns_default(self):
        """GET /api/admin/macro/status should return default enabled state."""
        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(return_value=None)

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get("/api/admin/macro/status")

        assert resp.status_code == 200
        data = resp.json()
        assert data["macro_enabled"] is True
        assert data["source"] == "default"

    @pytest.mark.asyncio
    async def test_get_macro_status_from_config(self):
        """GET /api/admin/macro/status should read from risk_configs."""
        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(return_value=FakeRecord({
            "macro_enabled": "false",
        }))

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get("/api/admin/macro/status")

        assert resp.status_code == 200
        data = resp.json()
        assert data["macro_enabled"] is False
        assert data["source"] == "risk_configs"

    @pytest.mark.asyncio
    async def test_toggle_macro_layer(self):
        """PUT /api/admin/macro/toggle should persist state and record audit."""
        config_id = str(uuid4())
        mock_pool = AsyncMock()
        # First call: fetch current state
        mock_pool.fetchrow = AsyncMock(return_value=FakeRecord({
            "id": config_id,
            "macro_enabled": "true",
        }))
        mock_pool.execute = AsyncMock()

        with patch("services.api.app.pool", mock_pool), \
             patch("services.api.app.record_audit_event", new_callable=AsyncMock) as mock_audit:
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.put(
                    "/api/admin/macro/toggle",
                    json={"enabled": False, "operator": "test_user"},
                )

        assert resp.status_code == 200
        data = resp.json()
        assert data["macro_enabled"] is False
        assert data["previous_enabled"] is True
        assert data["toggled_by"] == "test_user"

        # Verify audit event was recorded
        mock_audit.assert_called_once()
        audit_call = mock_audit.call_args
        assert audit_call.kwargs.get("event_type") or audit_call.args[1] == "macro.layer_toggled"


# ---------------------------------------------------------------------------
# Trend projection endpoint (Requirement: 12.10)
# ---------------------------------------------------------------------------


class TestTrendProjectionEndpoint:
    """Test trend projection endpoint returns projection data."""

    @pytest.mark.asyncio
    async def test_get_trend_projection(self):
        """GET /api/trends/{id}/projection should return projection data."""
        trend_id = str(uuid4())
        proj_row = _make_projection_row(trend_id)

        mock_pool = AsyncMock()
        # First call: verify trend exists
        mock_pool.fetchrow = AsyncMock(side_effect=[
            FakeRecord({"id": trend_id}),  # trend exists
            proj_row,  # projection data
        ])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get(f"/api/trends/{trend_id}/projection")

        assert resp.status_code == 200
        data = resp.json()
        assert data["projected_direction"] == "bearish"
        assert data["projected_strength"] == 0.6
        assert data["projected_confidence"] == 0.5
        assert data["diverges_from_current"] is True
        assert isinstance(data["driving_factors"], list)

    @pytest.mark.asyncio
    async def test_get_trend_projection_not_found(self):
        """GET /api/trends/{id}/projection should return null projection for missing."""
        trend_id = str(uuid4())
        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(side_effect=[
            FakeRecord({"id": trend_id}),  # trend exists
            None,  # no projection
        ])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get(f"/api/trends/{trend_id}/projection")

        assert resp.status_code == 200
        data = resp.json()
        assert data["projection"] is None

    @pytest.mark.asyncio
    async def test_get_trend_projection_trend_not_found(self):
        """GET /api/trends/{id}/projection should 404 for missing trend."""
        mock_pool = AsyncMock()
        mock_pool.fetchrow = AsyncMock(return_value=None)

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get(f"/api/trends/{uuid4()}/projection")

        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Macro impacts for ticker endpoint (Requirement: 8.2)
# ---------------------------------------------------------------------------


class TestMacroImpactsEndpoint:
    """Test macro impacts for a specific company."""

    @pytest.mark.asyncio
    async def test_get_macro_impacts_for_ticker(self):
        """GET /api/macro/impacts/{ticker} should return impact records."""
        impact_row = _make_impact_row(str(uuid4()))
        mock_pool = AsyncMock()
        # fetchrow returns None (no exposure profile)
        mock_pool.fetchrow = AsyncMock(return_value=None)
        mock_pool.fetch = AsyncMock(return_value=[impact_row])

        with patch("services.api.app.pool", mock_pool):
            transport = ASGITransport(app=app)
            async with AsyncClient(transport=transport, base_url="http://test") as client:
                resp = await client.get("/api/macro/impacts/AAPL")

        assert resp.status_code == 200
        data = resp.json()
        assert data["exposure_profile"] is None
        assert isinstance(data["impacts"], list)
        assert len(data["impacts"]) == 1
        assert data["impacts"][0]["ticker"] == "AAPL"
        assert data["impacts"][0]["macro_impact_score"] == 0.45
        assert data["impacts"][0]["impact_direction"] == "negative"
