# #region Test.Maintenance.Routes.Edge [C:3] [TYPE Module] [SEMANTICS test,maintenance,routes,edge,coverage]
# @BRIEF Edge-case tests for Maintenance Banner API routes — covers uncovered branches from _routes.py.
# @RELATION BINDS_TO -> [MaintenanceRoutesModule]
# @TEST_EDGE: settings_not_found -> GET /settings returns 404 when no settings row exists
# @TEST_EDGE: settings_upsert -> PUT /settings creates new settings when none exist
# @TEST_EDGE: settings_all_fields -> PUT /settings with all fields updates correctly
# @TEST_EDGE: end_already_completed -> POST /{id}/end returns idempotent success for already-completed
# @TEST_EDGE: start_past_time -> POST /start with start_time too far in past returns 400
# @TEST_EDGE: start_html_message -> POST /start with HTML in message is accepted
# @TEST_EDGE: end_all_with_env_body -> POST /end-all with environment_id in body
# @TEST_EDGE: banners_with_linked_states -> GET /dashboard-banners with linked states/events
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
import pytest

# ── Shared fixtures ────────────────────────────────────────────


@pytest.fixture
def client():
    from fastapi.testclient import TestClient
    from src.app import app
    return TestClient(app)


@pytest.fixture(autouse=True)
def auth_mock(client, mock_db):
    from src.app import app
    from src.core.auth.api_key import generate_api_key
    from src.dependencies import get_current_user
    from src.models.api_key import APIKey

    raw, prefix, key_hash = generate_api_key()
    api_key = APIKey(
        key_hash=key_hash, prefix=prefix, name="Test Key",
        permissions=["maintenance:start", "maintenance:end", "maintenance:end_all"],
        active=True,
    )
    mock_db.add(api_key)
    mock_db.commit()
    client.headers.update({"X-API-Key": raw})

    mock_role = MagicMock()
    mock_role.name = "Admin"
    mock_role.permissions = []

    mock_user = MagicMock()
    mock_user.username = "test-admin"
    mock_user.roles = [mock_role]

    async def _mock_get_current_user():
        return mock_user

    app.dependency_overrides[get_current_user] = _mock_get_current_user

    yield

    client.headers.pop("X-API-Key", None)
    app.dependency_overrides = {}


@pytest.fixture
def mock_db():
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    from sqlalchemy.pool import StaticPool
    from src.dependencies import get_db
    from src.models.mapping import Base

    engine = create_engine(
        "sqlite:///:memory:",
        poolclass=StaticPool,
        connect_args={"check_same_thread": False},
    )
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    def _get_db_override():
        db = Session()
        try:
            yield db
        finally:
            db.close()

    from src.app import app
    app.dependency_overrides[get_db] = _get_db_override
    yield session
    app.dependency_overrides.pop(get_db, None)
    session.close()


@pytest.fixture
def mock_task_manager():
    from src.dependencies import get_task_manager

    from unittest.mock import AsyncMock
    mock_tm = MagicMock()
    mock_task = MagicMock()
    mock_task.id = "test-task-id-edge"
    mock_tm.create_task = AsyncMock(return_value=mock_task)

    from src.app import app
    app.dependency_overrides[get_task_manager] = lambda: mock_tm
    yield mock_tm
    app.dependency_overrides.pop(get_task_manager, None)


# ── GET /api/maintenance/settings — not found ─────────────────


class TestSettingsNotFound:
    """GET /api/maintenance/settings when no settings exist."""

    # #region test_get_settings_not_found [C:2] [TYPE Function]
    # @BRIEF No MaintenanceSettings row -> 404.
    # @TEST_EDGE: settings_not_found -> GET /settings returns 404
    def test_get_settings_not_found(self, client, mock_db, mock_task_manager):
        # Remove the default settings created by the fixture
        from src.models.maintenance import MaintenanceSettings
        mock_db.query(MaintenanceSettings).delete()
        mock_db.commit()

        response = client.get("/api/maintenance/settings")
        assert response.status_code == 404
        assert "not configured" in response.json()["detail"]
    # #endregion test_get_settings_not_found


# ── PUT /api/maintenance/settings — upsert (no existing) ──────


class TestSettingsUpsert:
    """PUT /api/maintenance/settings when no settings row exists."""

    # #region test_upsert_settings [C:2] [TYPE Function]
    # @BRIEF No settings row -> upsert creates one.
    def test_upsert_settings(self, client, mock_db, mock_task_manager):
        from src.models.maintenance import MaintenanceSettings
        mock_db.query(MaintenanceSettings).delete()
        mock_db.commit()

        response = client.put(
            "/api/maintenance/settings",
            json={"target_environment_id": "new-env", "display_timezone": "Asia/Tokyo"},
        )
        assert response.status_code == 200
        data = response.json()
        assert data["target_environment_id"] == "new-env"
        assert data["display_timezone"] == "Asia/Tokyo"
    # #endregion test_upsert_settings

    # #region test_upsert_settings_all_fields [C:2] [TYPE Function]
    # @BRIEF All fields provided -> upsert with complete data.
    def test_upsert_settings_all_fields(self, client, mock_db, mock_task_manager):
        from src.models.maintenance import MaintenanceSettings
        mock_db.query(MaintenanceSettings).delete()
        mock_db.commit()

        response = client.put(
            "/api/maintenance/settings",
            json={
                "target_environment_id": "env-2",
                "display_timezone": "Europe/London",
                "banner_template": "Custom: {message}",
                "dashboard_scope": "all",
                "excluded_dashboard_ids": [1, 2],
                "forced_dashboard_ids": [3],
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert data["target_environment_id"] == "env-2"
        assert data["display_timezone"] == "Europe/London"
        assert data["banner_template"] == "Custom: {message}"
        assert data["dashboard_scope"] == "all"
        assert 1 in data["excluded_dashboard_ids"]
        assert 3 in data["forced_dashboard_ids"]
    # #endregion test_upsert_settings_all_fields


# ── POST /api/maintenance/{id}/end — already completed ────────


class TestEndAlreadyCompleted:
    """POST /api/maintenance/{id}/end when event is already COMPLETED."""

    # #region test_end_idempotent_completed [C:2] [TYPE Function]
    # @BRIEF Event already COMPLETED -> returns 202 with already_completed status.
    def test_end_idempotent_completed(self, client, mock_db, mock_task_manager):
        from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus

        event = MaintenanceEvent(
            tables=["raw.sales"],
            start_time=datetime.now(UTC),
            status=MaintenanceEventStatus.COMPLETED,
            environment_id="test-env",
            task_id="existing-task",
        )
        mock_db.add(event)
        mock_db.commit()

        response = client.post(f"/api/maintenance/{event.id}/end")
        assert response.status_code == 202
        data = response.json()
        assert data["status"] == "already_completed"
        assert data["task_id"] == "existing-task"
    # #endregion test_end_idempotent_completed


# ── POST /api/maintenance/start — edge cases ──────────────────


class TestStartEdgeCases:
    """POST /api/maintenance/start — edge paths."""

    # #region test_start_too_far_past [C:2] [TYPE Function]
    # @BRIEF start_time more than 1h in the past -> 400.
    def test_start_too_far_past(self, client, mock_db, mock_task_manager):
        response = client.post(
            "/api/maintenance/start",
            json={
                "tables": ["raw.sales"],
                "start_time": (datetime.now(UTC) - timedelta(hours=5)).isoformat(),
                "environment_id": "test-env",
            },
        )
        assert response.status_code == 400
        assert "too far in the past" in response.json()["detail"]
    # #endregion test_start_too_far_past

    # #region test_start_with_html_message [C:2] [TYPE Function]
    # @BRIEF Message with HTML content -> accepted (HTML will be escaped at render).
    def test_start_with_html_message(self, client, mock_db, mock_task_manager):
        response = client.post(
            "/api/maintenance/start",
            json={
                "tables": ["raw.sales"],
                "start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
                "end_time": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),
                "message": "<script>alert('xss')</script>",
                "environment_id": "test-env",
            },
        )
        # HTML in message is accepted — rendering layer escapes it
        assert response.status_code == 202
    # #endregion test_start_with_html_message


# ── POST /api/maintenance/end-all — with env in body ──────────


class TestEndAllWithEnv:
    """POST /api/maintenance/end-all with environment_id in body."""

    # #region test_end_all_with_env_body [C:2] [TYPE Function]
    # @BRIEF environment_id in JSON body -> passed to task params.
    def test_end_all_with_env_body(self, client, mock_db, mock_task_manager):
        response = client.post(
            "/api/maintenance/end-all",
            json={"environment_id": "env-42"},
        )
        assert response.status_code == 202
        data = response.json()
        assert data["status"] == "pending"
        # Verify environment_id was passed through
        call_kwargs = mock_task_manager.create_task.call_args[1]
        assert call_kwargs["params"]["environment_id"] == "env-42"
    # #endregion test_end_all_with_env_body


# ── GET /api/maintenance/dashboard-banners — with linked states ─


class TestBannersWithLinkedStates:
    """GET /api/maintenance/dashboard-banners with linked dashboard states and events."""

    # #region test_banners_with_linked_states [C:2] [TYPE Function]
    # @BRIEF Banners with linked MaintenanceDashboardState records -> events aggregated.
    def test_banners_with_linked_states(self, client, mock_db, mock_task_manager):
        from src.models.maintenance import (
            MaintenanceDashboardBanner,
            MaintenanceDashboardBannerStatus,
            MaintenanceDashboardState,
            MaintenanceDashboardStateStatus,
            MaintenanceEvent,
            MaintenanceEventStatus,
        )

        event = MaintenanceEvent(
            tables=["raw.test"],
            start_time=datetime.now(UTC),
            end_time=datetime.now(UTC) + timedelta(hours=2),
            status=MaintenanceEventStatus.ACTIVE,
            environment_id="test-env",
        )
        mock_db.add(event)
        mock_db.flush()

        banner = MaintenanceDashboardBanner(
            environment_id="test-env",
            dashboard_id=201,
            chart_id=67890,
            banner_text="Linked test",
            status=MaintenanceDashboardBannerStatus.ACTIVE,
        )
        mock_db.add(banner)
        mock_db.flush()

        state = MaintenanceDashboardState(
            banner_id=banner.id,
            event_id=event.id,
            dashboard_id=201,
            status=MaintenanceDashboardStateStatus.ACTIVE,
        )
        mock_db.add(state)
        mock_db.commit()

        response = client.get("/api/maintenance/dashboard-banners")
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        assert len(data) >= 1
        banners_for_dash = [b for b in data if b["dashboard_id"] == 201]
        assert len(banners_for_dash) == 1
        assert len(banners_for_dash[0]["events"]) == 1
        assert banners_for_dash[0]["active"] is True
    # #endregion test_banners_with_linked_states


# ── GET /api/maintenance/dashboard-banners — no linked states ─


class TestBannersNoLinkedStates:
    """GET /api/maintenance/dashboard-banners with no linked states (just banner)."""

    # #region test_banners_no_linked_states [C:2] [TYPE Function]
    # @BRIEF Active banner without linked states -> event lists are empty.
    def test_banners_no_linked_states(self, client, mock_db, mock_task_manager):
        from src.models.maintenance import (
            MaintenanceDashboardBanner,
            MaintenanceDashboardBannerStatus,
        )

        banner = MaintenanceDashboardBanner(
            environment_id="test-env",
            dashboard_id=301,
            chart_id=11111,
            banner_text="No linked state",
            status=MaintenanceDashboardBannerStatus.ACTIVE,
        )
        mock_db.add(banner)
        mock_db.commit()

        response = client.get("/api/maintenance/dashboard-banners")
        assert response.status_code == 200
        data = response.json()
        banners_for_dash = [b for b in data if b["dashboard_id"] == 301]
        assert len(banners_for_dash) == 1
        assert banners_for_dash[0]["events"] == []
    # #endregion test_banners_no_linked_states
# #endregion Test.Maintenance.Routes.Edge
