# #region Test.Maintenance.ChartManager [C:3] [TYPE Module] [SEMANTICS test, maintenance, chart, banner, edge]
# @BRIEF Tests for maintenance chart manager — ensure_banner_chart, process_dashboards, process_states.
# @RELATION BINDS_TO -> [MaintenanceChartManager]

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.models.maintenance import (
    MaintenanceDashboardBanner,
    MaintenanceDashboardBannerStatus,
    MaintenanceDashboardState,
    MaintenanceDashboardStateStatus,
    MaintenanceEvent,
    MaintenanceEventStatus,
    MaintenanceSettings,
    DashboardScope,
)


@pytest.fixture
def mock_db():
    db = MagicMock()
    db.query.return_value.filter.return_value.first.return_value = None
    db.query.return_value.filter.return_value.count.return_value = 0
    db.query.return_value.filter.return_value.all.return_value = []
    return db


@pytest.fixture
def mock_superset():
    client = AsyncMock()
    client.get_chart.return_value = {"id": 123, "result": {"viz_type": "markdown"}}
    client.create_markdown_chart.return_value = 456
    client.update_dashboard_layout.return_value = {"result": "ok"}
    client.update_banner_on_dashboard.return_value = {"result": "ok"}
    client.remove_chart_from_layout.return_value = {"result": "ok"}
    client.delete_chart.return_value = {"result": "ok"}
    return client


class TestEnsureBannerChart:
    @pytest.mark.asyncio
    async def test_creates_new_banner(self, mock_db, mock_superset):
        """No existing banner → creates chart, updates layout, persists banner."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        result = await ensure_banner_chart(1, "env1", mock_superset, mock_db)
        assert result.chart_id == 456
        assert result.status == MaintenanceDashboardBannerStatus.ACTIVE
        mock_superset.create_markdown_chart.assert_called_once_with(1, "*Maintenance banner placeholder*")
        mock_superset.update_dashboard_layout.assert_called_once_with(1, 456)
        mock_db.add.assert_called_once()
        mock_db.flush.assert_called()

    @pytest.mark.asyncio
    async def test_reuses_existing_banner(self, mock_db, mock_superset):
        """Existing active banner → verifies chart in Superset and reuses."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        existing = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, environment_id="env1",
            chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text=""
        )
        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = existing
        mock_superset.get_chart.return_value = {"id": 123}

        result = await ensure_banner_chart(1, "env1", mock_superset, mock_db)
        assert result is existing
        mock_superset.get_chart.assert_called_once_with(123)
        mock_superset.create_markdown_chart.assert_not_called()

    @pytest.mark.asyncio
    async def test_stale_banner_removed_and_recreated(self, mock_db, mock_superset):
        """Existing but stale banner → marked REMOVED, new one created."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        existing = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, environment_id="env1",
            chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text=""
        )
        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = existing
        mock_superset.get_chart.side_effect = Exception("Chart not found")

        result = await ensure_banner_chart(1, "env1", mock_superset, mock_db)
        assert existing.status == MaintenanceDashboardBannerStatus.REMOVED
        assert result.chart_id == 456  # new chart
        mock_superset.create_markdown_chart.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_chart_failure(self, mock_db, mock_superset):
        """Failure to create chart → exception propagates."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        mock_superset.create_markdown_chart.side_effect = Exception("API error")
        with pytest.raises(Exception, match="API error"):
            await ensure_banner_chart(1, "env1", mock_superset, mock_db)

    @pytest.mark.asyncio
    async def test_layout_update_failure_cleans_up_chart(self, mock_db, mock_superset):
        """Failure to update layout → deletes chart and raises."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        mock_superset.update_dashboard_layout.side_effect = Exception("Layout error")
        with pytest.raises(Exception, match="Layout error"):
            await ensure_banner_chart(1, "env1", mock_superset, mock_db)
        mock_superset.delete_chart.assert_called_once_with(456)

    @pytest.mark.asyncio
    async def test_layout_update_failure_cleanup_ignores_delete_error(self, mock_db, mock_superset):
        """Delete chart failure during cleanup does not mask original error."""
        from src.services.maintenance._chart_manager import ensure_banner_chart

        mock_superset.update_dashboard_layout.side_effect = Exception("Layout error")
        mock_superset.delete_chart.side_effect = Exception("Delete also failed")
        with pytest.raises(Exception, match="Layout error"):
            await ensure_banner_chart(1, "env1", mock_superset, mock_db)


class TestProcessDashboardsForStart:
    @pytest.mark.asyncio
    async def test_successful_processing(self, mock_db, mock_superset):
        """All dashboards processed successfully."""
        from src.services.maintenance._chart_manager import _process_dashboards_for_start

        mock_superset.get_dashboards.return_value = (1, [{"id": 1, "dashboard_title": "Test"}])

        successful, failed, banners = await _process_dashboards_for_start(
            [1], "evt1", "env1", "Template: {message}", "UTC", mock_superset, mock_db
        )
        assert len(successful) == 1
        assert len(failed) == 0
        assert len(banners) == 1

    @pytest.mark.asyncio
    async def test_banner_update_failure_handled(self, mock_db, mock_superset):
        """Failed banner update → records as APPLY_FAILED."""
        from src.services.maintenance._chart_manager import _process_dashboards_for_start

        mock_superset.update_banner_on_dashboard.side_effect = Exception("Update failed")
        mock_superset.get_dashboards.return_value = (1, [{"id": 1, "dashboard_title": "Test"}])

        successful, failed, banners = await _process_dashboards_for_start(
            [1], "evt1", "env1", "Template", "UTC", mock_superset, mock_db
        )
        assert len(successful) == 0
        assert len(failed) == 1
        assert len(banners) == 1

    @pytest.mark.asyncio
    async def test_top_level_exception_handling(self, mock_db, mock_superset):
        """Exception outside banner creation → recorded as failed."""
        from src.services.maintenance._chart_manager import _process_dashboards_for_start

        mock_superset.create_markdown_chart.side_effect = Exception("Chart creation failed")

        successful, failed, banners = await _process_dashboards_for_start(
            [1], "evt1", "env1", "Template", "UTC", mock_superset, mock_db
        )
        assert len(successful) == 0
        assert len(failed) == 1


class TestProcessStatesForEnd:
    @pytest.mark.asyncio
    async def test_no_banner_id(self, mock_db, mock_superset):
        """State with no banner_id → marked REMOVED."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, status=MaintenanceDashboardStateStatus.ACTIVE
        )
        state.banner_id = None
        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 1
        assert state.status == MaintenanceDashboardStateStatus.REMOVED

    @pytest.mark.asyncio
    async def test_other_events_still_active_rebuilds(self, mock_db, mock_superset):
        """Other active events exist → rebuild banner."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        banner = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, chart_id=123,
            status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old"
        )
        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="b1",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )

        # Mock query: other active events exist
        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 1
        mock_db.query.return_value.filter.return_value.first.return_value = banner

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 1
        assert state.status == MaintenanceDashboardStateStatus.REMOVED

    @pytest.mark.asyncio
    async def test_no_other_events_removes_banner(self, mock_db, mock_superset):
        """No other active events → remove banner fully."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        banner = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, chart_id=123,
            status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old"
        )
        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="b1",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )

        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0
        mock_db.query.return_value.filter.return_value.first.return_value = banner

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 1
        assert banner.status == MaintenanceDashboardBannerStatus.REMOVED
        assert state.status == MaintenanceDashboardStateStatus.REMOVED
        mock_superset.remove_chart_from_layout.assert_called_once_with(1, 123)
        mock_superset.delete_chart.assert_called_once_with(123)

    @pytest.mark.asyncio
    async def test_remove_chart_failure(self, mock_db, mock_superset):
        """Failure to remove chart from layout → REMOVAL_FAILED."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        banner = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, chart_id=123,
            status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old"
        )
        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="b1",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )

        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0
        mock_db.query.return_value.filter.return_value.first.return_value = banner
        mock_superset.remove_chart_from_layout.side_effect = Exception("Remove failed")

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 0
        assert len(failed) == 1
        assert state.status == MaintenanceDashboardStateStatus.REMOVAL_FAILED

    @pytest.mark.asyncio
    async def test_delete_chart_failure(self, mock_db, mock_superset):
        """Failure to delete chart → REMOVAL_FAILED."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        banner = MaintenanceDashboardBanner(
            id="b1", dashboard_id=1, chart_id=123,
            status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text="old"
        )
        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="b1",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )

        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0
        mock_db.query.return_value.filter.return_value.first.return_value = banner
        mock_superset.delete_chart.side_effect = Exception("Delete failed")

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 0
        assert len(failed) == 1
        assert state.status == MaintenanceDashboardStateStatus.REMOVAL_FAILED

    @pytest.mark.asyncio
    async def test_no_banner_record(self, mock_db, mock_superset):
        """State with banner_id but no banner record → REMOVED."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="nonexistent",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )
        mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.count.return_value = 0
        mock_db.query.return_value.filter.return_value.first.return_value = None

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 1
        assert state.status == MaintenanceDashboardStateStatus.REMOVED

    @pytest.mark.asyncio
    async def test_top_level_exception(self, mock_db, mock_superset):
        """Exception at top level → REMOVAL_FAILED."""
        from src.services.maintenance._chart_manager import _process_states_for_end

        state = MaintenanceDashboardState(
            event_id="evt1", dashboard_id=1, banner_id="b1",
            status=MaintenanceDashboardStateStatus.ACTIVE
        )
        mock_db.query.side_effect = Exception("DB error")

        removed, failed = await _process_states_for_end([state], "evt1", mock_superset, mock_db)
        assert removed == 0
        assert len(failed) == 1
# #endregion Test.Maintenance.ChartManager
