"""Core SQLite run history store tests covering CRUD flows."""

from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from importlib import import_module
from pathlib import Path
import pytest
from orcheo_backend.app.history import (
    RunHistoryError,
    RunHistoryNotFoundError,
    SqliteRunHistoryStore,
)


sqlite_store_module = import_module("orcheo_backend.app.history.sqlite_store")


@dataclass
class _EmbeddingLike:
    values: list[float]


@pytest.mark.asyncio
async def test_sqlite_store_persists_history(tmp_path: Path) -> None:
    db_path = tmp_path / "history.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(
        workflow_id="wf",
        execution_id="exec",
        inputs={"foo": "bar"},
    )
    await store.append_step("exec", {"status": "running"})
    await store.mark_completed("exec")

    history = await store.get_history("exec")
    assert history.status == "completed"
    assert history.inputs == {"foo": "bar"}
    assert len(history.steps) == 1
    assert history.steps[0].payload == {"status": "running"}

    store_reloaded = SqliteRunHistoryStore(str(db_path))
    persisted = await store_reloaded.get_history("exec")
    assert persisted.status == "completed"
    assert persisted.steps[0].payload == {"status": "running"}


@pytest.mark.asyncio
async def test_sqlite_store_records_trace_metadata(tmp_path: Path) -> None:
    db_path = tmp_path / "history.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    started_at = datetime.now(tz=UTC)

    await store.start_run(
        workflow_id="wf",
        execution_id="exec",
        trace_id="trace-456",
        trace_started_at=started_at,
    )
    step = await store.append_step("exec", {"status": "running"})
    await store.mark_completed("exec")

    history = await store.get_history("exec")
    assert history.trace_id == "trace-456"
    assert history.trace_started_at == started_at
    assert history.trace_last_span_at == step.at
    assert history.trace_completed_at is not None


@pytest.mark.asyncio
async def test_sqlite_store_persists_runnable_config(tmp_path: Path) -> None:
    db_path = tmp_path / "history-config.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(
        workflow_id="wf",
        execution_id="exec",
        runnable_config={"metadata": {"foo": "bar"}},
        tags=["alpha"],
        callbacks=[{"name": "cb"}],
        metadata={"foo": "bar"},
        run_name="demo",
    )

    history = await store.get_history("exec")
    assert history.runnable_config["metadata"] == {"foo": "bar"}
    assert history.tags == ["alpha"]
    assert history.callbacks == [{"name": "cb"}]
    assert history.metadata == {"foo": "bar"}
    assert history.run_name == "demo"


@pytest.mark.asyncio
async def test_sqlite_store_wraps_non_list_callbacks(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    db_path = tmp_path / "history-config-wrap.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    original_normalize = sqlite_store_module.normalize_json_value

    def _normalize_with_scalar(value):
        if value == ["config-callback"]:
            return "config-callback"
        return original_normalize(value)

    monkeypatch.setattr(
        sqlite_store_module, "normalize_json_value", _normalize_with_scalar
    )

    await store.start_run(
        workflow_id="wf",
        execution_id="exec",
        runnable_config={"callbacks": ["config-callback"]},
    )

    history = await store.get_history("exec")
    assert history.callbacks == ["config-callback"]


@pytest.mark.asyncio
async def test_sqlite_store_duplicate_execution_id_raises(
    tmp_path: Path,
) -> None:
    db_path = tmp_path / "history-dupe.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")

    with pytest.raises(RunHistoryError, match="execution_id=exec"):
        await store.start_run(workflow_id="wf", execution_id="exec")


@pytest.mark.asyncio
async def test_sqlite_store_append_step_missing_execution_raises(
    tmp_path: Path,
) -> None:
    db_path = tmp_path / "history-append.sqlite"
    store = SqliteRunHistoryStore(str(db_path))

    with pytest.raises(RunHistoryNotFoundError, match="execution_id=missing"):
        await store.append_step("missing", {"action": "start"})


@pytest.mark.asyncio
async def test_sqlite_store_append_step_normalizes_non_json_values(
    tmp_path: Path,
) -> None:
    db_path = tmp_path / "history-serialize.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")

    await store.append_step(
        "exec",
        {"embedding": _EmbeddingLike(values=[0.1, 0.2])},
    )

    history = await store.get_history("exec")
    assert history.steps[0].payload == {"embedding": {"values": [0.1, 0.2]}}


@pytest.mark.asyncio
async def test_sqlite_list_histories_filters_by_workflow(tmp_path: Path) -> None:
    db_path = tmp_path / "history-list.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf-a", execution_id="exec-1")
    await store.start_run(workflow_id="wf-b", execution_id="exec-2")

    results = await store.list_histories("wf-a")
    assert [record.execution_id for record in results] == ["exec-1"]

    limited = await store.list_histories("wf-a", limit=1)
    assert len(limited) == 1
    assert limited[0].execution_id == "exec-1"


@pytest.mark.asyncio
async def test_sqlite_store_mark_completed(tmp_path: Path) -> None:
    db_path = tmp_path / "history-complete.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")

    result = await store.mark_completed("exec")
    assert result.status == "completed"
    assert result.completed_at is not None
    assert result.error is None

    history = await store.get_history("exec")
    assert history.status == "completed"


@pytest.mark.asyncio
async def test_sqlite_store_mark_completed_sets_trace_last_span(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """mark_completed should backfill trace_last_span_at when row lacks a value."""

    db_path = tmp_path / "history-trace.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")
    original_row_to_record = sqlite_store_module.row_to_record

    def _force_missing_trace_span(row, steps):
        record = original_row_to_record(row, steps)
        record.trace_last_span_at = None
        return record

    monkeypatch.setattr(sqlite_store_module, "row_to_record", _force_missing_trace_span)

    result = await store.mark_completed("exec")
    monkeypatch.setattr(sqlite_store_module, "row_to_record", original_row_to_record)

    assert result.trace_last_span_at == result.trace_completed_at


@pytest.mark.asyncio
async def test_sqlite_store_mark_failed(tmp_path: Path) -> None:
    db_path = tmp_path / "history-failed.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")

    result = await store.mark_failed("exec", "boom")
    assert result.status == "error"
    assert result.error == "boom"
    assert result.completed_at is not None

    history = await store.get_history("exec")
    assert history.status == "error"
    assert history.error == "boom"


@pytest.mark.asyncio
async def test_sqlite_store_mark_cancelled(tmp_path: Path) -> None:
    db_path = tmp_path / "history-cancelled.sqlite"
    store = SqliteRunHistoryStore(str(db_path))
    await store.start_run(workflow_id="wf", execution_id="exec")

    result = await store.mark_cancelled("exec", reason="shutdown")
    assert result.status == "cancelled"
    assert result.error == "shutdown"
    assert result.completed_at is not None

    history = await store.get_history("exec")
    assert history.status == "cancelled"
    assert history.error == "shutdown"
