import re

import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError

from qualpipe_webapp.frontend import main as frontend_main
from qualpipe_webapp.frontend import page_config as frontend_page_config
from qualpipe_webapp.frontend.page_config import (
    FrontendConfig,
    LayoutConfig,
    PageConfig,
    PageIndexEntry,
    adapt_backend_payload,
    build_nav_pages,
    build_plot_state,
    load_frontend_config,
)


def test_load_frontend_config_valid(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    pages_dir = tmp_path / "pages"
    pages_dir.mkdir()
    page_path = pages_dir / "custom_page.yaml"
    page_path.write_text(
        """
name: custom_page
label: Custom Page
plots:
    - title: Plot A
      metric: some_metric
      plotType: scatterplot
      x:
        label: Pixel ID
        scale: linear
      y:
        label: Plot A
        scale: linear
      line: none
layout:
  rows: 1
  cols: 1
  max_per_row: 1
css:
  - badger
js:
  - d3
"""
    )

    config_path = tmp_path / "pages_index.yaml"
    config_path.write_text(
        """
auxiliary_subitems:
  - Lidar
pages:
  - name: custom_page
    source: pages/custom_page.yaml
"""
    )

    config = load_frontend_config(config_path)
    assert config.pages[0].name == "custom_page"
    assert config.pages[0].layout.rows == 1


def test_load_frontend_config_invalid(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    config_path = tmp_path / "pages_index.yaml"
    config_path.write_text("auxiliary_subitems: []\n")

    with pytest.raises(ValidationError):
        load_frontend_config(config_path)


def _build_custom_config():
    # Minimal config used to isolate behavior in tests.
    return FrontendConfig(
        array_element_types=["LSTs", "MSTs", "SSTs"],
        auxiliary_subitems=["Lidar"],
        pages=[
            PageConfig(
                name="custom_page",
                label="Custom Page",
                plots=[
                    {
                        "plotType": "scatterplot",
                        "title": "Plot A",
                        "metric": "some_metric",
                        "x": {"label": "Pixel ID", "scale": "linear"},
                        "y": {"label": "Plot A", "scale": "linear"},
                        "line": "none",
                    }
                ],
                layout=LayoutConfig(rows=1, cols=1, max_per_row=1),
                css=["badger"],
                js=["d3"],
            )
        ],
        page_entries=[
            PageIndexEntry(name="custom_page", source="pages/custom_page.yaml")
        ],
    )


def _patch_config(monkeypatch, config):
    # Force the app to use the provided config for this test.
    monkeypatch.setattr(frontend_main, "load_frontend_config", lambda: config)
    # Keep navbar globals in sync with the patched config.
    frontend_main.view.env.globals["nav_pages"] = build_nav_pages(config)
    frontend_main.view.env.globals["nav_auxiliary"] = [
        {"name": name, "label": name} for name in config.auxiliary_subitems
    ]


def test_route_uses_config_for_pages(monkeypatch):
    config = _build_custom_config()
    _patch_config(monkeypatch, config)
    client = TestClient(frontend_main.app)

    response = client.get("/LSTs/custom_page")
    assert response.status_code == 200
    assert "Custom Page" in response.text
    assert 'data-plot-key="' in response.text

    nav_pattern = r"href=\"/LSTs/custom_page\"[\w\s=:;<>\".\-]*>Custom Page</a"
    assert re.search(nav_pattern, response.text)


def test_unknown_page_returns_404(monkeypatch):
    config = _build_custom_config()
    _patch_config(monkeypatch, config)
    client = TestClient(frontend_main.app)

    response = client.get("/LSTs/pointings")
    assert response.status_code == 404


def test_auxiliary_subitems_from_config(monkeypatch):
    config = _build_custom_config()
    _patch_config(monkeypatch, config)
    client = TestClient(frontend_main.app)

    response = client.get("/Auxiliary/FRAM")
    assert response.status_code == 404

    response = client.get("/Auxiliary/Lidar")
    assert response.status_code == 501


def test_user_index_merges_pages_and_overrides_aux(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    default_pages = tmp_path / "default_pages"
    default_pages.mkdir()
    (default_pages / "default_page.yaml").write_text(
        """
name: default_page
label: Default Page
plots:
  - title: Plot A
    metric: some_metric
    plotType: scatterplot
    x:
      label: Pixel ID
      scale: linear
    y:
      label: Plot A
      scale: linear
    line: none
"""
    )
    user_pages = tmp_path / "user_pages"
    user_pages.mkdir()
    (user_pages / "custom_page.yaml").write_text(
        """
name: custom_page
label: Custom Page
plots:
  - title: Plot B
    metric: some_metric
    plotType: scatterplot
    x:
      label: Pixel ID
      scale: linear
    y:
      label: Plot B
      scale: linear
    line: none
"""
    )
    (user_pages / "pages_index.yaml").write_text(
        """
pages:
  - name: custom_page
    source: user_pages/custom_page.yaml
auxiliary_subitems:
  - Lidar
"""
    )
    (tmp_path / "pages_index.yaml").write_text(
        """
pages:
  - name: default_page
    source: default_pages/default_page.yaml
auxiliary_subitems:
  - Lidar
  - FRAM
"""
    )

    config = load_frontend_config(tmp_path / "pages_index.yaml")
    assert [page.name for page in config.pages] == ["default_page", "custom_page"]
    assert config.auxiliary_subitems == ["Lidar"]


def test_user_index_conflicting_page_name_is_rejected(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    default_pages = tmp_path / "default_pages"
    default_pages.mkdir()
    (default_pages / "shared.yaml").write_text(
        """
name: shared_page
label: Shared Page (Default)
plots:
  - title: Plot A
    metric: default_metric
    plotType: scatterplot
    x:
      label: Pixel ID
      scale: linear
    y:
      label: Plot A
      scale: linear
    line: none
"""
    )
    user_pages = tmp_path / "user_pages"
    user_pages.mkdir()
    (user_pages / "shared.yaml").write_text(
        """
name: shared_page
label: Shared Page (User)
plots:
  - title: Plot B
    metric: user_metric
    plotType: scatterplot
    x:
      label: Pixel ID
      scale: linear
    y:
      label: Plot B
      scale: linear
    line: none
"""
    )
    (user_pages / "pages_index.yaml").write_text(
        """
pages:
  - name: shared_page
    source: user_pages/shared.yaml
"""
    )
    (tmp_path / "pages_index.yaml").write_text(
        """
pages:
  - name: shared_page
    source: default_pages/shared.yaml
"""
    )

    config = load_frontend_config(tmp_path / "pages_index.yaml")
    assert [page.name for page in config.pages] == ["shared_page"]
    assert config.pages[0].label == "Shared Page (Default)"
    assert config.user_index_error is not None
    assert "conflicts with an existing default page" in config.user_index_error


def test_invalid_user_index_falls_back(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    default_pages = tmp_path / "default_pages"
    default_pages.mkdir()
    (default_pages / "default_page.yaml").write_text(
        """
name: default_page
label: Default Page
plots:
  - title: Plot A
    metric: some_metric
    plotType: scatterplot
    x:
      label: Pixel ID
      scale: linear
    y:
      label: Plot A
      scale: linear
    line: none
"""
    )
    user_pages = tmp_path / "user_pages"
    user_pages.mkdir()
    (user_pages / "pages_index.yaml").write_text("pages: 123\n")
    (tmp_path / "pages_index.yaml").write_text(
        """
pages:
  - name: default_page
    source: default_pages/default_page.yaml
auxiliary_subitems:
  - Lidar
"""
    )

    config = load_frontend_config(tmp_path / "pages_index.yaml")
    assert config.pages[0].name == "default_page"
    assert config.user_index_error is not None


def test_build_plot_state_uses_inline_configs():
    page = PageConfig(
        name="custom_page",
        label="Custom Page",
        plots=[
            {
                "plotType": "scatterplot",
                "metric": "some_metric",
                "title": "Plot A (Gain 0)",
                "x": {"label": "Pixel ID", "scale": "linear"},
                "y": {"label": "Override Y"},
                "line": "none",
            }
        ],
        layout=LayoutConfig(rows=1, cols=1, max_per_row=1),
        css=["badger"],
        js=["d3"],
    )

    plot_configs, plot_errors = build_plot_state(page)
    full_key = page.plot_keys()[0]
    assert plot_configs[full_key]["plotType"] == "scatterplot"
    assert plot_configs[full_key]["y"]["label"] == "Override Y"
    assert plot_configs[full_key]["x"]["scale"] == "linear"
    assert plot_errors == {}


def test_adapt_backend_payload_flattens_nested_column_data():
    payload = {
        "pedestal_charge_mean": {
            "fetchedData": {
                "n_rows": 1,
                "columns": {
                    "mean": {
                        "data": [[[1.1, 2.2, 3.3]]],
                    }
                },
            },
            "fetchedMetadata": {},
        }
    }

    plot_configurations = {
        "pedestal_charge_mean_cameraview": {
            "plotType": "cameraview",
            "title": "Pedestal",
            "metric": "pedestal_charge_mean",
            "x": {
                "label": "X",
                "field": "mean",
                "scale": "linear",
            },
        }
    }

    adapted = adapt_backend_payload(payload, plot_configurations)
    plot_payload = adapted["pedestal_charge_mean_cameraview"]

    assert plot_payload["fetchedData"]["x"] == [1.1, 2.2, 3.3]


def test_adapt_backend_payload_scatter_uses_index_when_x_field_missing():
    payload = {
        "metric_a": {
            "fetchedData": {
                "columns": {
                    "mean": {"data": [10.0, 20.0, 30.0]},
                }
            },
            "fetchedMetadata": {"source": "backend"},
        }
    }
    plot_configurations = {
        "metric_a_scatter": {
            "plotType": "scatterplot",
            "title": "Scatter",
            "metric": "metric_a",
            "x": {"label": "Index", "scale": "linear"},
            "y": {"label": "Mean", "scale": "linear", "field": "mean"},
            "line": "none",
        }
    }

    adapted = adapt_backend_payload(payload, plot_configurations)
    assert adapted["metric_a_scatter"]["fetchedData"]["x"] == [0, 1, 2]
    assert adapted["metric_a_scatter"]["fetchedData"]["y"] == [10.0, 20.0, 30.0]
    assert adapted["metric_a_scatter"]["fetchedMetadata"] == {"source": "backend"}


def test_adapt_backend_payload_histogram_converts_m_to_cm():
    payload = {
        "metric_b": {
            "fetchedData": {
                "columns": {
                    "some_length": {"data": [1.0, 2.0], "unit": "m"},
                }
            },
            "fetchedMetadata": {},
        }
    }
    plot_configurations = {
        "metric_b_hist": {
            "plotType": "histogram1d",
            "title": "Histogram",
            "metric": "metric_b",
            "x": {
                "label": "Some Length",
                "scale": "linear",
                "field": "some_length",
                "unit": "cm",
            },
            "y": {"label": "Count", "scale": "linear"},
            "bins": 10,
        }
    }

    adapted = adapt_backend_payload(payload, plot_configurations)
    assert adapted["metric_b_hist"]["fetchedData"]["x"] == [100.0, 200.0]


def test_adapt_backend_payload_camera_includes_gain_when_present():
    payload = {
        "metric_c": {
            "fetchedData": {
                "columns": {
                    "mean": {"data": [0.1, 0.2, 0.3]},
                    "gain": {"data": [[1, 0, 1]]},
                }
            },
            "fetchedMetadata": {},
        }
    }
    plot_configurations = {
        "metric_c_cam": {
            "plotType": "cameraview",
            "title": "Camera",
            "metric": "metric_c",
            "x": {"label": "Mean", "scale": "linear", "field": "mean"},
        }
    }

    adapted = adapt_backend_payload(payload, plot_configurations)
    assert adapted["metric_c_cam"]["fetchedData"]["x"] == [0.1, 0.2, 0.3]
    assert adapted["metric_c_cam"]["fetchedData"]["gain"] == [1, 0, 1]


def test_adapt_backend_payload_returns_original_when_no_plot_can_be_adapted():
    payload = {
        "metric_d": {
            "fetchedData": {"columns": {"mean": {"data": [1, 2]}}},
            "fetchedMetadata": {},
        }
    }
    # Invalid config (missing required metric field for adapter validation)
    plot_configurations = {
        "bad_plot": {
            "plotType": "scatterplot",
            "title": "Bad",
            "x": {"label": "X", "scale": "linear", "field": "mean"},
            "y": {"label": "Y", "scale": "linear", "field": "mean"},
            "line": "none",
        }
    }

    adapted = adapt_backend_payload(payload, plot_configurations)
    assert adapted == payload


def test_invalid_plot_config_rejected(tmp_path, monkeypatch):
    load_frontend_config.cache_clear()
    monkeypatch.setattr(
        frontend_page_config,
        "_user_index_path",
        lambda: tmp_path / "user_pages" / "pages_index.yaml",
    )
    pages_dir = tmp_path / "pages"
    pages_dir.mkdir()
    (pages_dir / "custom_page.yaml").write_text(
        """
name: custom_page
label: Custom Page
plots:
  - title: Plot A
    metric: some_metric
    plotType: scatterplot
    y:
      label: Plot A
      scale: linear
"""
    )
    (tmp_path / "pages_index.yaml").write_text(
        """
pages:
    - name: custom_page
      source: pages/custom_page.yaml
"""
    )

    config = load_frontend_config(tmp_path / "pages_index.yaml")
    assert [page.name for page in config.pages] == ["custom_page"]

    plot_configs, plot_errors = frontend_page_config.build_plot_state(config.pages[0])
    assert plot_configs == {}
    assert config.pages[0].plot_keys()[0] in plot_errors


def test_plot_title_required():
    with pytest.raises(ValidationError):
        PageConfig(
            name="custom_page",
            label="Custom Page",
            plots=[{"plotType": "scatterplot", "metric": "some_metric"}],
            layout=LayoutConfig(rows=1, cols=1, max_per_row=1),
            css=["badger"],
            js=["d3"],
        )


def test_plot_keys_are_generated_from_metric_and_plot_type():
    page = PageConfig(
        name="custom_page",
        label="Custom Page",
        plots=[
            {
                "title": "Plot A",
                "plotType": "scatterplot",
                "metric": "foo_mean",
            },
            {
                "title": "Plot A",
                "plotType": "scatterplot",
                "metric": "foo_mean",
            },
        ],
        layout=LayoutConfig(rows=1, cols=1, max_per_row=1),
        css=["badger"],
        js=["d3"],
    )

    plot_keys = page.plot_keys()
    assert plot_keys == ["foo_mean_scatterplot", "foo_mean_scatterplot_2"]


def _page_with_plot(plot):
    plots = plot if isinstance(plot, list) else [plot]
    return PageConfig(
        name="custom_page",
        label="Custom Page",
        plots=plots,
        layout=LayoutConfig(rows=1, cols=1, max_per_row=1),
        css=["badger"],
        js=["d3"],
    )


def test_scatterplot_config_valid_and_dump_aliases():
    page = _page_with_plot(
        {
            "plotType": "scatterplot",
            "title": "Plot A",
            "metric": "some_metric",
            "x": {"label": "Pixel ID", "scale": "linear"},
            "y": {"label": "Plot A", "scale": "time"},
            "line": "dashed",
            "marks": {"strokeWidth": 2, "fill": "#000"},
        },
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_errors == {}
    full_key = page.plot_keys()[0]
    assert plot_configs[full_key]["plotType"] == "scatterplot"
    assert plot_configs[full_key]["marks"]["strokeWidth"] == 2


def test_plot_source_required():
    page = _page_with_plot({"title": "Placeholder"})
    assert page.plot_keys() == ["placeholder"]


def test_scatterplot_rejects_invalid_line():
    page = _page_with_plot(
        {
            "plotType": "scatterplot",
            "title": "Plot A",
            "metric": "some_metric",
            "x": {"label": "Pixel ID", "scale": "linear"},
            "y": {"label": "Plot A", "scale": "linear"},
            "line": "zigzag",
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_configs == {}
    assert page.plot_keys()[0] in plot_errors


def test_cameraview_config_valid():
    page = _page_with_plot(
        {
            "plotType": "cameraview",
            "metric": "some_metric",
            "title": "Camera view",
            "x": {"domain": [0.0, 1.0]},
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_errors == {}
    full_key = page.plot_keys()[0]
    assert plot_configs[full_key]["plotType"] == "cameraview"


def test_cameraview_rejects_marks():
    page = _page_with_plot(
        {
            "plotType": "cameraview",
            "title": "Camera view",
            "metric": "some_metric",
            "x": {"domain": [0.0, 1.0]},
            "marks": {"fill": "#000"},
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_configs == {}
    assert page.plot_keys()[0] in plot_errors


def test_histogram_config_valid():
    page = _page_with_plot(
        {
            "plotType": "histogram1d",
            "title": "Histogram",
            "metric": "some_metric",
            "x": {"label": "Value", "scale": "log"},
            "y": {"label": "Count", "scale": "linear"},
            "bins": 10,
            "marks": {"strokeWidth": 1, "opacity": 0.5},
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_errors == {}
    full_key = page.plot_keys()[0]
    assert plot_configs[full_key]["plotType"] == "histogram1d"


def test_histogram_rejects_invalid_scale():
    page = _page_with_plot(
        {
            "plotType": "histogram1d",
            "title": "Histogram",
            "metric": "some_metric",
            "x": {"label": "Value", "scale": "time"},
            "y": {"label": "Count", "scale": "linear"},
            "bins": 10,
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_configs == {}
    assert page.plot_keys()[0] in plot_errors


def test_plot_config_rejects_unknown_keys():
    page = _page_with_plot(
        {
            "plotType": "scatterplot",
            "title": "Plot A",
            "metric": "some_metric",
            "x": {"label": "Pixel ID", "scale": "linear", "xscale": "linear"},
            "y": {"label": "Plot A", "scale": "linear"},
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_configs == {}
    assert page.plot_keys()[0] in plot_errors


def test_plot_config_rejects_wrong_types():
    page = _page_with_plot(
        {
            "plotType": "scatterplot",
            "title": "Plot A",
            "metric": "some_metric",
            "x": {"label": True, "scale": "linear"},
            "y": {"label": "Plot A", "scale": "linear"},
        }
    )

    plot_configs, plot_errors = build_plot_state(page)
    assert plot_configs == {}
    assert page.plot_keys()[0] in plot_errors


def test_home_shows_user_index_error(monkeypatch):
    config = _build_custom_config()
    config.user_index_error = "Bad user index"
    _patch_config(monkeypatch, config)
    client = TestClient(frontend_main.app)

    response = client.get("/home")
    assert response.status_code == 200
    assert "User configuration error" in response.text
    assert "Bad user index" in response.text


def test_invalid_page_shows_error_page(monkeypatch):
    config = _build_custom_config()
    config.page_errors["custom_page"] = "Invalid config"
    config.pages = []
    _patch_config(monkeypatch, config)
    client = TestClient(frontend_main.app)

    response = client.get("/LSTs/custom_page")
    assert response.status_code == 500
    assert "Page configuration error" in response.text
    assert "Invalid config" in response.text
