"""End-to-end smoke tests for Temporal mode.

Drive the live timeline against the Temporal fixture DB (see conftest's
TEMPORAL_SETUP): the source x timeop controls, the During window frame, the
axis date caption, and the empty-union marker."""
from __future__ import annotations

import re

from playwright.sync_api import Page, expect


def _goto_temporal(page: Page, url: str) -> None:
    page.goto(url + "/temporal")
    expect(page.locator("body")).to_have_class("mode-temporal", timeout=5000)


def test_relation_full_renders_all_lanes(page: Page, temporal_studio_url: str) -> None:
    """Relation source, Full: the three Prime-Minister terms each get a lane
    and a bar."""
    _goto_temporal(page, temporal_studio_url)
    page.locator("#temporal-relation").select_option("cs4_holds")
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)
    expect(page.locator(".cv-temporal__bar")).to_have_count(3, timeout=8000)


def test_relation_asof_filters_to_one(page: Page, temporal_studio_url: str) -> None:
    """As of 2018-06-15 only the 2016-2022 term is valid: a single lane."""
    _goto_temporal(page, temporal_studio_url)
    page.locator("#temporal-relation").select_option("cs4_holds")
    page.locator('.cv-temporal__op[data-timeop="asof"]').click()
    page.locator("#temporal-at").fill("2018-06-15T00:00")
    page.locator("#temporal-at").dispatch_event("change")
    expect(page.locator(".cv-temporal__lane")).to_have_count(1, timeout=8000)


def test_during_draws_window_frame(page: Page, temporal_studio_url: str) -> None:
    """During [2014, 2017): the two overlapping terms show (full, unclipped
    bars) and the window frame draws two dimming masks and two edges."""
    _goto_temporal(page, temporal_studio_url)
    page.locator("#temporal-relation").select_option("cs4_holds")
    page.locator('.cv-temporal__op[data-timeop="during"]').click()
    page.locator("#temporal-from").fill("2014-01-01T00:00")
    page.locator("#temporal-from").dispatch_event("change")
    page.locator("#temporal-to").fill("2017-01-01T00:00")
    page.locator("#temporal-to").dispatch_event("change")
    expect(page.locator(".cv-temporal__lane")).to_have_count(2, timeout=8000)
    expect(page.locator(".cv-temporal__wmask")).to_have_count(2, timeout=8000)
    expect(page.locator(".cv-temporal__wedge")).to_have_count(2)


def test_query_source_shows_axis_date_caption(
    page: Page, temporal_studio_url: str
) -> None:
    """Query source over the minute-scale sensor data: the axis caption
    surfaces the date the clock-time ticks omit."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM sensor")
    # Selecting the mapping triggers the fetch (reading the query box).
    page.locator("#temporal-mapping").select_option("public.sensor_validity")
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)
    expect(page.locator(".cv-temporal__axisctx")).to_have_text("2024-03-15", timeout=8000)


def test_order_by_start_resorts_lanes(
    page: Page, temporal_studio_url: str
) -> None:
    """The Order control re-sorts the lanes client-side by validity. A query
    deliberately returned in reverse-chronological order (id DESC over the
    three Prime-Minister terms) shows id 3 first; switching Order to 'by start'
    floats the earliest term (id 1) to the top."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM cs4_holds ORDER BY id DESC")
    page.locator("#temporal-mapping").select_option("provsql.time_validity_view")
    lane0 = page.locator(".cv-temporal__lane").first.locator(".cv-temporal__lanelbl")
    expect(lane0).to_contain_text("3", timeout=8000)   # query order: id 3 (2022) first
    page.locator("#temporal-sort").select_option("start")
    expect(lane0).to_contain_text("1")                 # by start: id 1 (2010) first


def test_reload_restores_query_source(
    page: Page, temporal_studio_url: str
) -> None:
    """A plain page reload restores the Temporal UI (source, query, time op)
    rather than silently falling back to the Relation display."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM sensor")
    page.locator("#temporal-mapping").select_option("public.sensor_validity")
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)

    page.reload(wait_until="networkidle")
    # Still in Query source, query preserved, timeline redrawn -- not reverted.
    expect(page.locator('.cv-temporal__src[data-source="query"]')).to_have_class(
        re.compile(r"\bis-active\b"), timeout=8000)
    expect(page.locator("#request")).to_have_value("SELECT * FROM sensor")
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)


def test_failed_query_clears_timeline(
    page: Page, temporal_studio_url: str
) -> None:
    """A query that errors must erase the timeline, not leave the previous
    result on screen. A query source falls back to running the query for the
    result table, so the error surfaces there rather than on the timeline."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM sensor")
    page.locator("#temporal-mapping").select_option("public.sensor_validity")
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)

    # A failing query (unknown relation): no stale lanes, and the real error
    # shows in the result area (via the /api/exec fallback).
    page.locator("#request").fill("SELECT * FROM no_such_table_xyz")
    page.locator("#request").dispatch_event("change")
    expect(page.locator(".cv-temporal__lane")).to_have_count(0, timeout=8000)
    expect(page.locator("#result-banners")).to_contain_text("does not exist", timeout=8000)


def test_query_runs_exactly_once(
    page: Page, temporal_studio_url: str
) -> None:
    """A Send in Temporal mode executes the underlying query exactly once,
    via /api/temporal (which also fills the result table), never a second
    time via /api/exec -- which would double-run a side-effecting query such
    as SELECT undo(...). Also checks the result table is rendered from that
    single response (the query columns plus a trailing provsql column)."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT reading FROM sensor")
    page.locator("#temporal-mapping").select_option("public.sensor_validity")
    expect(page.locator(".cv-temporal__lane").first).to_be_visible(timeout=8000)

    calls = {"exec": 0, "temporal": 0}

    def _count(r):
        if "/api/exec" in r.url:
            calls["exec"] += 1
        elif "/api/temporal" in r.url:
            calls["temporal"] += 1

    page.on("request", _count)
    page.locator("#run-btn").click()
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)
    page.wait_for_timeout(500)
    assert calls == {"exec": 0, "temporal": 1}, calls
    # Result table came from the temporal response: query column + provsql.
    cols = page.eval_on_selector_all("#result-head th", "e => e.map(x => x.textContent)")
    assert cols == ["reading", "provsql"], cols


def test_non_temporal_query_still_shows_output(
    page: Page, temporal_studio_url: str
) -> None:
    """A query that cannot be placed on a timeline (no provenance column) still
    shows its output in the result table, with an empty timeline (it falls back
    to running the query for the result table)."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT 42 AS answer")
    page.locator("#temporal-mapping").select_option("provsql.time_validity_view")
    page.locator("#run-btn").click()
    expect(page.locator(".cv-temporal__lane")).to_have_count(0, timeout=8000)
    expect(page.locator("#result-body")).to_contain_text("42", timeout=8000)


def test_empty_union_renders_never_marker(
    page: Page, temporal_studio_url: str
) -> None:
    """The row with an empty validity union renders the '∅ never' marker
    rather than a bar."""
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM emptyval")
    page.locator("#temporal-mapping").select_option("public.emptyval_validity")
    expect(page.locator(".cv-temporal__never")).to_be_visible(timeout=8000)


def test_stale_response_does_not_overwrite_a_newer_one(
    page: Page, temporal_studio_url: str
) -> None:
    """A late /api/temporal response must not redraw the timeline over a
    newer one.

    On load the Temporal UI is in Relation source and auto-fetches; that
    request can still be in flight while the user switches to Query source
    and runs their own. The handler used to read `state` at completion
    time, so whichever response landed last won the timeline, the query box
    and the persisted state. Hold the relation request, let the query one
    through, then release the stale response and check nothing moved.
    """
    held: list = []

    def handler(route):
        data = route.request.post_data or ""
        if '"relation"' in data and not held:
            held.append(route)          # hold, release later
        else:
            route.continue_()

    page.route("**/api/temporal", handler)
    _goto_temporal(page, temporal_studio_url)
    page.locator('.cv-temporal__src[data-source="query"]').click()
    page.locator('.cv-temporal__op[data-timeop="full"]').click()
    page.locator("#request").fill("SELECT * FROM sensor")
    page.locator("#temporal-mapping").select_option("public.sensor_validity")
    expect(page.locator(".cv-temporal__lane")).to_have_count(3, timeout=8000)
    lanes_before = page.locator(".cv-temporal__lanelbl").all_inner_texts()

    # Release the superseded relation response.
    assert held, "the init's relation auto-fetch was never issued"
    held[0].continue_()
    page.wait_for_timeout(1500)

    expect(page.locator("#request")).to_have_value("SELECT * FROM sensor")
    assert page.locator(".cv-temporal__lanelbl").all_inner_texts() == lanes_before
