# #region Test.DashboardTopology [C:3] [TYPE Module] [SEMANTICS test,topology,dashboard,superset]
# @BRIEF Comprehensive tests for _topology.py — chart name/params, topology building, layout parsing.
# @RELATION BINDS_TO -> [DashboardTopology]
# @TEST_EDGE: chart_name_fallback -> Falls back to title or "Chart id" when slice_name missing
# @TEST_EDGE: params_string_parse -> String params parsed, invalid string returns {}
# @TEST_EDGE: describe_empty_optional -> No metrics/groupby/filters returns empty string
# @TEST_EDGE: resolve_child_string -> String ref resolved from pos_json, missing ref returns None
# @TEST_EDGE: topology_without_grid -> No GRID/ROOT nodes falls back to flat iteration
# @TEST_EDGE: build_topology_string_posjson -> position_json as string is parsed
import json
from unittest.mock import MagicMock


# ── _chart_name ──

class TestChartName:
    """Verify _chart_name."""

    def test_slice_name_used(self):
        from src.plugins.llm_analysis._topology import _chart_name
        assert _chart_name({"slice_name": "Revenue"}) == "Revenue"

    def test_title_fallback(self):
        from src.plugins.llm_analysis._topology import _chart_name
        assert _chart_name({"title": "Chart Title"}) == "Chart Title"

    def test_slice_name_overrides_title(self):
        from src.plugins.llm_analysis._topology import _chart_name
        assert _chart_name({"slice_name": "Actual", "title": "Fallback"}) == "Actual"

    def test_id_fallback(self):
        from src.plugins.llm_analysis._topology import _chart_name
        result = _chart_name({"id": 42})
        assert result == "Chart 42"

    def test_empty_dict(self):
        from src.plugins.llm_analysis._topology import _chart_name
        result = _chart_name({})
        assert result == "Chart None"


# ── _chart_params ──

class TestChartParams:
    """Verify _chart_params."""

    def test_dict_params(self):
        from src.plugins.llm_analysis._topology import _chart_params
        result = _chart_params({"params": {"metrics": ["count"]}})
        assert result == {"metrics": ["count"]}

    def test_string_params(self):
        from src.plugins.llm_analysis._topology import _chart_params
        result = _chart_params({"params": '{"metrics": ["count"]}'})
        assert result == {"metrics": ["count"]}

    def test_invalid_string_params(self):
        from src.plugins.llm_analysis._topology import _chart_params
        result = _chart_params({"params": "not-json"})
        assert result == {}

    def test_empty_params(self):
        from src.plugins.llm_analysis._topology import _chart_params
        assert _chart_params({}) == {}

    def test_none_params(self):
        from src.plugins.llm_analysis._topology import _chart_params
        # When params is None, returns None (params value as-is)
        assert _chart_params({"params": None}) is None


# ── _describe_chart_dataset ──

class TestDescribeChartDataset:
    """Verify _describe_chart_dataset."""

    def test_with_datasource_id(self):
        from src.plugins.llm_analysis._topology import _describe_chart_dataset
        result = _describe_chart_dataset({"datasource_id": 5}, {})
        assert "Dataset: 5" in result

    def test_datasource_id_in_params(self):
        from src.plugins.llm_analysis._topology import _describe_chart_dataset
        result = _describe_chart_dataset({}, {"datasource_id": 10})
        assert "Dataset: 10" in result

    def test_no_datasource(self):
        from src.plugins.llm_analysis._topology import _describe_chart_dataset
        assert _describe_chart_dataset({}, {}) == ""


# ── _describe_chart_metrics ──

class TestDescribeChartMetrics:
    """Verify _describe_chart_metrics."""

    def test_dict_metrics(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        result = _describe_chart_metrics({"metrics": [{"label": "SUM(revenue)"}]})
        assert "SUM(revenue)" in result

    def test_string_metrics(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        result = _describe_chart_metrics({"metrics": ["count"]})
        assert "count" in result

    def test_dict_metric_expression_fallback(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        result = _describe_chart_metrics({"metrics": [{"expression": "SUM(x)"}]})
        assert "SUM(x)" in result

    def test_empty_metrics(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        assert _describe_chart_metrics({"metrics": []}) == ""

    def test_missing_metrics(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        assert _describe_chart_metrics({}) == ""

    def test_truncated_to_three(self):
        from src.plugins.llm_analysis._topology import _describe_chart_metrics
        metrics = [str(i) for i in range(10)]
        result = _describe_chart_metrics({"metrics": metrics})
        assert len(result.split(",")) <= 4  # "Metrics: " prefix + 3 items max


# ── _describe_chart_groupby ──

class TestDescribeChartGroupby:
    """Verify _describe_chart_groupby."""

    def test_with_groupby(self):
        from src.plugins.llm_analysis._topology import _describe_chart_groupby
        result = _describe_chart_groupby({"groupby": ["category", "region"]})
        assert "category" in result
        assert "region" in result

    def test_truncated_to_three(self):
        from src.plugins.llm_analysis._topology import _describe_chart_groupby
        result = _describe_chart_groupby({"groupby": ["a", "b", "c", "d"]})
        assert "d" not in result

    def test_empty_groupby(self):
        from src.plugins.llm_analysis._topology import _describe_chart_groupby
        assert _describe_chart_groupby({"groupby": []}) == ""

    def test_missing_groupby(self):
        from src.plugins.llm_analysis._topology import _describe_chart_groupby
        assert _describe_chart_groupby({}) == ""


# ── _describe_chart_filters ──

class TestDescribeChartFilters:
    """Verify _describe_chart_filters."""

    def test_full_filter(self):
        from src.plugins.llm_analysis._topology import _describe_chart_filters
        result = _describe_chart_filters({
            "adhoc_filters": [
                {"subject": "revenue", "operator": ">", "comparator": "1000"}
            ]
        })
        assert "revenue" in result
        assert ">" in result

    def test_filter_without_subject_comparator(self):
        from src.plugins.llm_analysis._topology import _describe_chart_filters
        result = _describe_chart_filters({
            "adhoc_filters": [
                {"clause": "having"}
            ]
        })
        assert "having" in result

    def test_empty_filters(self):
        from src.plugins.llm_analysis._topology import _describe_chart_filters
        assert _describe_chart_filters({"adhoc_filters": []}) == ""

    def test_missing_filters(self):
        from src.plugins.llm_analysis._topology import _describe_chart_filters
        assert _describe_chart_filters({}) == ""

    def test_non_dict_filter(self):
        from src.plugins.llm_analysis._topology import _describe_chart_filters
        result = _describe_chart_filters({"adhoc_filters": ["raw_string"]})
        assert result == ""  # not a dict, no clause key


# ── _describe_chart ──

class TestDescribeChart:
    """Verify _describe_chart orchestration."""

    def test_describe_integration(self):
        from src.plugins.llm_analysis._topology import _describe_chart
        chart = {
            "slice_name": "Sales",
            "viz_type": "bar",
            "params": {"metrics": [{"label": "SUM(rev)"}]},
        }
        result = _describe_chart(chart)
        assert 'Chart "Sales" (bar)' in result
        assert "SUM(rev)" in result

    def test_describe_minimal(self):
        from src.plugins.llm_analysis._topology import _describe_chart
        result = _describe_chart({"id": 99})
        assert 'Chart "Chart 99" (unknown)' in result


# ── _sort_chart_key ──

class TestSortChartKey:
    """Verify _sort_chart_key."""

    def test_numeric_key(self):
        from src.plugins.llm_analysis._topology import _sort_chart_key
        result = _sort_chart_key(("42", {}))
        assert result == (0, 42)

    def test_string_key(self):
        from src.plugins.llm_analysis._topology import _sort_chart_key
        result = _sort_chart_key(("chart_1", {}))
        assert result == (1, "chart_1")

    def test_non_numeric_key(self):
        from src.plugins.llm_analysis._topology import _sort_chart_key
        result = _sort_chart_key(("abc", {}))
        assert result == (1, "abc")


# ── _resolve_child ──

class TestResolveChild:
    """Verify _resolve_child."""

    def test_dict_ref_returns_as_is(self):
        from src.plugins.llm_analysis._topology import _resolve_child
        ref = {"type": "CHART"}
        assert _resolve_child(ref, {}) is ref

    def test_string_ref_found(self):
        from src.plugins.llm_analysis._topology import _resolve_child
        pos_json = {"key1": {"type": "CHART"}}
        result = _resolve_child("key1", pos_json)
        assert result == {"type": "CHART"}

    def test_string_ref_not_found(self):
        from src.plugins.llm_analysis._topology import _resolve_child
        assert _resolve_child("missing", {}) is None


# ── _process_tab ──

class TestProcessTab:
    """Verify _process_tab."""

    def test_tab_with_children(self):
        from src.plugins.llm_analysis._topology import _process_tab
        tab_node = {
            "type": "TAB",
            "meta": {"text": "Revenue Tab"},
            "children": [
                {"type": "CHART", "meta": {"chartId": 1}},
            ]
        }
        result = _process_tab(tab_node, depth=1, pos_json={}, chart_lookup={"1": {"slice_name": "Rev Chart"}})
        assert len(result) == 2
        assert "Revenue Tab" in result[0]
        assert "Rev Chart" in result[1]

    def test_tab_meta_label_fallback(self):
        from src.plugins.llm_analysis._topology import _process_tab
        tab_node = {"type": "TAB", "meta": {"label": "Tab Label"}, "children": []}
        result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
        assert "Tab Label" in result[0]

    def test_tab_empty_meta(self):
        from src.plugins.llm_analysis._topology import _process_tab
        tab_node = {"children": []}
        result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
        assert "Tab" in result[0]


# ── _process_chart ──

class TestProcessChart:
    """Verify _process_chart."""

    def test_chart_found_in_lookup(self):
        from src.plugins.llm_analysis._topology import _process_chart
        child = {"meta": {"chartId": 5}}
        chart_lookup = {"5": {"slice_name": "Test Chart", "viz_type": "line"}}
        result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
        assert len(result) == 1
        assert "Test Chart" in result[0]

    def test_chart_not_in_lookup(self):
        from src.plugins.llm_analysis._topology import _process_chart
        child = {"meta": {"chartId": 99}}
        result = _process_chart(child, depth=0, chart_lookup={})
        assert result == []

    def test_chart_with_slice_id(self):
        from src.plugins.llm_analysis._topology import _process_chart
        child = {"meta": {"slice_id": 10}}
        chart_lookup = {"10": {"slice_name": "Slice Chart"}}
        result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
        assert len(result) == 1

    def test_chart_missing_meta(self):
        from src.plugins.llm_analysis._topology import _process_chart
        result = _process_chart({}, depth=0, chart_lookup={"1": {}})
        assert result == []


# ── _process_children ──

class TestProcessChildren:
    """Verify _process_children."""

    def test_tab_child(self):
        from src.plugins.llm_analysis._topology import _process_children
        parent = {
            "children": [
                {"type": "TAB", "meta": {"text": "Tab1"}, "children": []}
            ]
        }
        result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
        assert len(result) == 1
        assert "Tab1" in result[0]

    def test_row_child(self):
        from src.plugins.llm_analysis._topology import _process_children
        parent = {
            "children": [
                {"type": "ROW", "children": [
                    {"type": "CHART", "meta": {"chartId": 1}}
                ]}
            ]
        }
        chart_lookup = {"1": {"slice_name": "C1"}}
        result = _process_children(parent, depth=0, pos_json={}, chart_lookup=chart_lookup)
        assert len(result) == 1

    def test_unknown_type_skipped(self):
        from src.plugins.llm_analysis._topology import _process_children
        parent = {"children": [{"type": "HEADER", "meta": {}}]}
        result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
        assert result == []

    def test_child_ref_resolved(self):
        from src.plugins.llm_analysis._topology import _process_children
        parent = {"children": ["ref_key"]}
        pos_json = {"ref_key": {"type": "CHART", "meta": {"chartId": 1}}}
        chart_lookup = {"1": {"slice_name": "Ref C"}}
        result = _process_children(parent, depth=0, pos_json=pos_json, chart_lookup=chart_lookup)
        assert len(result) == 1

    def test_child_ref_not_resolved(self):
        from src.plugins.llm_analysis._topology import _process_children
        parent = {"children": ["missing_key"]}
        result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
        assert result == []

    def test_non_dict_parent(self):
        from src.plugins.llm_analysis._topology import _process_children
        result = _process_children("not_a_dict", depth=0, pos_json={}, chart_lookup={})
        assert result == []


# ── _extract_topology_children ──

class TestExtractTopologyChildren:
    """Verify _extract_topology_children."""

    def test_grid_nodes(self):
        from src.plugins.llm_analysis._topology import _extract_topology_children
        pos_json = {
            "GRID_ID": {"type": "GRID", "children": [
                {"type": "CHART", "meta": {"chartId": 1}}
            ]}
        }
        chart_lookup = {"1": {"slice_name": "GChart"}}
        result = _extract_topology_children(pos_json, chart_lookup)
        assert len(result) >= 1

    def test_no_grid_fallback(self):
        from src.plugins.llm_analysis._topology import _extract_topology_children
        pos_json = {
            "tab1": {"type": "TAB", "children": []},
            "DASHBOARD_VERSION_KEY": "v2",
        }
        result = _extract_topology_children(pos_json, chart_lookup={})
        assert isinstance(result, list)

    def test_no_grid_skips_version_key(self):
        from src.plugins.llm_analysis._topology import _extract_topology_children
        pos_json = {"DASHBOARD_VERSION_KEY": "v2"}
        result = _extract_topology_children(pos_json, chart_lookup={})
        assert result == []

    def test_empty_position_json(self):
        from src.plugins.llm_analysis._topology import _extract_topology_children
        assert _extract_topology_children({}, {}) == []


# ── build_dashboard_topology (integration) ──

class TestBuildDashboardTopology:
    """Verify build_dashboard_topology full function."""

    def test_happy_path(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "id": 1,
            "dashboard_title": "Sales Overview",
            "position_json": {
                "ROOT_ID": {
                    "type": "ROOT",
                    "children": [
                        {"type": "CHART", "meta": {"chartId": 10}}
                    ]
                }
            },
        }
        charts = [{"id": 10, "slice_name": "Revenue", "viz_type": "big_number"}]
        result = build_dashboard_topology(dashboard, charts)
        assert 'Dashboard: "Sales Overview"' in result
        assert "Revenue" in result
        assert "Charts: 1 total" in result

    def test_title_fallback(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {"title": "Alt Title", "position_json": {}}
        result = build_dashboard_topology(dashboard, [])
        assert "Alt Title" in result

    def test_no_title_fallback(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {"id": 5, "position_json": {}}
        result = build_dashboard_topology(dashboard, [])
        assert "Dashboard 5" in result

    def test_position_json_as_string(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "Test",
            "position_json": json.dumps({
                "ROOT_ID": {"type": "ROOT", "children": []}
            }),
        }
        result = build_dashboard_topology(dashboard, [])
        assert "Test" in result

    def test_position_json_invalid_string(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "Broken",
            "position_json": "not-valid-json{{",
        }
        result = build_dashboard_topology(dashboard, [])
        assert "Broken" in result  # gracefully handles parse failure

    def test_position_json_not_dict(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "NoPos",
            "position_json": [],
        }
        result = build_dashboard_topology(dashboard, [])
        assert "NoPos" in result

    def test_chart_lookup_with_slice_id(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "Test",
            "position_json": {},
        }
        charts = [{"slice_id": 1, "slice_name": "Slice Chart", "viz_type": "table"}]
        result = build_dashboard_topology(dashboard, charts)
        assert "Slice Chart" in result

    def test_topology_parse_warning(self):
        """Exception during _extract_topology_children is caught and noted."""
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "Err",
            "position_json": {
                "ROOT_ID": {"type": "ROOT", "children": ["missing"]}
            },
        }
        # missing key in position_json triggers no error since _resolve_child returns None
        result = build_dashboard_topology(dashboard, [])
        assert "Err" in result

    def test_charts_sorted_by_key(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {
            "dashboard_title": "Sorted",
            "position_json": {},
        }
        charts = [
            {"id": 10, "slice_name": "Z Chart"},
            {"id": 5, "slice_name": "A Chart"},
        ]
        result = build_dashboard_topology(dashboard, charts)
        # Both appear
        assert "A Chart" in result
        assert "Z Chart" in result

    def test_empty_charts(self):
        from src.plugins.llm_analysis._topology import build_dashboard_topology
        dashboard = {"dashboard_title": "Empty", "position_json": {}}
        result = build_dashboard_topology(dashboard, [])
        assert "Empty" in result
        assert "Charts: 0 total" not in result  # no chart_lookup when charts empty
# #endregion Test.DashboardTopology
