from __future__ import annotations

import hashlib
import json
from pathlib import Path

import pyarrow as pa
import pytest

from cf_datahive import DataHiveClient


def _make_client(tmp_path: Path) -> tuple[DataHiveClient, Path]:
    workspace_root = tmp_path / "workspace"
    workspace_root.mkdir()
    return DataHiveClient(str(workspace_root)), workspace_root


def test_create_run_creates_staged_manifest(tmp_path: Path) -> None:
    client, workspace_root = _make_client(tmp_path)

    run = client.create_run(
        pipeline_id="pipeline_alpha",
        pipeline_label="Pipeline Alpha",
        metadata={"source": "unit-test"},
    )

    manifest_path = run.run_path / "manifest.json"
    assert manifest_path.exists()
    assert str(run.session_path).startswith(str(workspace_root / "data_hive" / "pipeline_alpha"))
    assert run.session_id.startswith("session-")
    assert run.session_run_index == 0

    manifest = client.load_manifest("pipeline_alpha", run.run_id)
    assert manifest.schema_version == "1.0"
    assert manifest.status == "staged"
    assert manifest.pipeline_label == "Pipeline Alpha"
    assert manifest.metadata == {"source": "unit-test"}
    assert manifest.session_id == run.session_id
    assert manifest.session_run_index == run.session_run_index


def test_open_run_returns_existing_staged_handle(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    created = client.create_run("open_pipeline")

    reopened = client.open_run("open_pipeline", created.run_id)
    assert reopened.run_id == created.run_id
    assert reopened.pipeline_id == "open_pipeline"
    assert reopened.session_id == created.session_id
    assert reopened.session_run_index == created.session_run_index
    assert reopened.manifest.status == "staged"

    client.write_table(reopened, "metrics", pa.table({"value": [1]}))
    manifest = client.load_manifest("open_pipeline", created.run_id)
    assert manifest.tables["metrics"].n_rows == 1


def test_write_table_append_creates_parts_and_updates_manifest(tmp_path: Path) -> None:
    client, workspace_root = _make_client(tmp_path)
    run = client.create_run("append_pipeline")

    table_a = pa.table({"value": [1, 2, 3]})
    entry_a = client.write_table(run, "metrics", table_a, chunk_size=2)

    table_b = pa.table({"value": [4, 5]})
    entry_b = client.write_table(run, "metrics", table_b, mode="append")

    table_dir = run.session_path / "tables" / "metrics" / f"run_id={run.run_id}"
    parts = sorted(path.name for path in table_dir.glob("part-*.parquet"))

    assert entry_a.n_rows == 3
    assert entry_a.n_files == 2
    assert entry_b.n_rows == 5
    assert entry_b.n_files == 3
    assert parts == ["part-0000.parquet", "part-0001.parquet", "part-0002.parquet"]

    manifest = client.load_manifest("append_pipeline", run.run_id)
    assert manifest.tables["metrics"].n_rows == 5
    assert manifest.tables["metrics"].n_files == 3


def test_write_table_overwrite_replaces_files(tmp_path: Path) -> None:
    client, workspace_root = _make_client(tmp_path)
    run = client.create_run("overwrite_pipeline")

    client.write_table(run, "metrics", pa.table({"value": [1, 2, 3]}), chunk_size=1)
    overwrite_entry = client.write_table(
        run,
        "metrics",
        pa.table({"value": [10, 11]}),
        mode="overwrite",
    )

    table_dir = run.session_path / "tables" / "metrics" / f"run_id={run.run_id}"
    parts = sorted(path.name for path in table_dir.glob("part-*.parquet"))

    assert overwrite_entry.n_rows == 2
    assert overwrite_entry.n_files == 1
    assert parts == ["part-0000.parquet"]


def test_write_artifact_hash_and_size(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    run = client.create_run("artifact_pipeline")

    payload = b"artifact payload"
    expected_hash = hashlib.sha256(payload).hexdigest()

    entry = client.write_artifact(
        run,
        artifact_name="report.bin",
        payload=payload,
        media_type="application/octet-stream",
        step_id="step.report",
    )

    assert entry.sha256 == expected_hash
    assert entry.size_bytes == len(payload)
    assert client.open_artifact("artifact_pipeline", run.run_id, "report.bin") == payload


def test_commit_updates_status_and_latest(tmp_path: Path) -> None:
    client, workspace_root = _make_client(tmp_path)
    run = client.create_run("commit_pipeline")

    table = pa.table({"value": [1, 2, 3]})
    client.write_table(run, "metrics", table)

    payload = b"commit artifact"
    expected_hash = hashlib.sha256(payload).hexdigest()
    client.write_artifact(run, "artifact.bin", payload)

    client.commit_run(run)

    manifest = client.load_manifest("commit_pipeline", run.run_id)
    latest_path = workspace_root / "data_hive" / "commit_pipeline" / "latest.txt"

    assert manifest.status == "committed"
    assert latest_path.read_text(encoding="utf-8") == run.run_id
    assert manifest.tables["metrics"].n_rows == 3
    assert manifest.tables["metrics"].n_files == 1
    assert manifest.tables["metrics"].file_hashes is not None
    assert len(manifest.tables["metrics"].file_hashes or []) == 1
    assert manifest.artifacts["artifact.bin"].sha256 == expected_hash
    assert manifest.session_id == run.session_id
    assert manifest.session_run_index == run.session_run_index


def test_read_table_roundtrip_matches(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    run = client.create_run("roundtrip_pipeline")

    source = pa.table({"value": [10, 20], "label": ["a", "b"]})
    client.write_table(run, "metrics", source)
    client.commit_run(run)

    loaded = client.read_table("roundtrip_pipeline", run.run_id, "metrics")
    assert loaded.to_pydict() == source.to_pydict()


def test_sanitization_blocks_path_traversal(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)

    with pytest.raises(ValueError):
        client.create_run("../escape")

    run = client.create_run("safe_pipeline")

    with pytest.raises(ValueError):
        client.write_table(run, "../table", pa.table({"value": [1]}))

    with pytest.raises(ValueError):
        client.write_artifact(run, "../artifact.bin", b"x")

    with pytest.raises(ValueError):
        client.load_manifest("safe_pipeline", "../run")


def test_integration_full_lifecycle_end_to_end(tmp_path: Path) -> None:
    pandas = pytest.importorskip("pandas")
    client, _ = _make_client(tmp_path)

    run = client.create_run(
        pipeline_id="integration_pipeline",
        pipeline_label="Integration Pipeline",
        metadata={"batch": "B42"},
    )

    dataframe = pandas.DataFrame(
        {
            "ts": [100, 200],
            "value": [0.12, 0.34],
        }
    )
    client.write_table(
        run,
        table_name="measurements",
        data=dataframe,
        step_id="step.measure",
        chunk_size=1,
    )
    client.write_artifact(
        run,
        artifact_name="blob.bin",
        payload=b"\x00\x01\x02",
        media_type="application/octet-stream",
        step_id="step.blob",
    )

    client.commit_run(run)

    summaries = client.list_runs("integration_pipeline")
    manifest = client.load_manifest("integration_pipeline", run.run_id)
    table = client.read_table("integration_pipeline", run.run_id, "measurements")
    blob = client.open_artifact("integration_pipeline", run.run_id, "blob.bin")

    assert summaries
    assert summaries[0].run_id == run.run_id
    assert summaries[0].session_id == run.session_id
    assert summaries[0].session_run_index == run.session_run_index
    assert manifest.status == "committed"
    assert manifest.tables["measurements"].file_hashes is not None
    assert table.num_rows == 2
    assert blob == b"\x00\x01\x02"


def test_storage_mode_lock_is_strict_per_session_table(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    first = client.create_run("mode_lock_pipeline")
    client.write_table(
        first,
        "metrics",
        pa.table({"value": [1]}),
        storage_mode="partitioned_by_run",
    )
    client.commit_run(first)

    session_manifest_path = first.session_path / "session_manifest.json"
    payload = json.loads(session_manifest_path.read_text(encoding="utf-8"))
    assert payload["tables"]["metrics"]["locked_storage_mode"] == "partitioned_by_run"

    second = client.create_run("mode_lock_pipeline")
    with pytest.raises(ValueError, match="storage mode lock violation"):
        client.write_table(
            second,
            "metrics",
            pa.table({"value": [2]}),
            storage_mode="compacted_session",
        )


def test_auto_mode_compacted_session_and_run_filtered_reads(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    run_ids: list[str] = []
    for index in range(12):
        run = client.create_run("sensor_pipeline")
        client.write_table(
            run,
            "sensor_rows",
            pa.table({"value": [index]}),
            storage_mode="auto",
            rows_per_run_estimate=1,
            run_count_estimate=1000,
        )
        client.commit_run(run)
        run_ids.append(run.run_id)

    latest_run_id = run_ids[-1]
    latest_manifest = client.load_manifest("sensor_pipeline", latest_run_id)
    assert latest_manifest.tables["sensor_rows"].locked_storage_mode == "compacted_session"

    latest_run = client.open_run("sensor_pipeline", latest_run_id)
    table_root = latest_run.session_path / "tables" / "sensor_rows"
    compacted_dir = table_root / "_session_compacted"
    assert compacted_dir.exists()
    assert not list(table_root.glob("run_id=*"))
    assert 1 <= len(list(compacted_dir.glob("part-*.parquet"))) <= 32

    session_table = client.read_table(
        "sensor_pipeline",
        latest_run_id,
        "sensor_rows",
        read_scope="session",
    )
    assert session_table.num_rows == 12

    first_run_table = client.read_table("sensor_pipeline", run_ids[0], "sensor_rows")
    assert first_run_table.num_rows == 1
    assert first_run_table.column("run_id").to_pylist() == [run_ids[0]]


def test_auto_mode_partitioned_by_run_and_session_reads(tmp_path: Path) -> None:
    client, _ = _make_client(tmp_path)
    run_ids: list[str] = []
    for _ in range(3):
        run = client.create_run("measurement_pipeline")
        values = list(range(1000))
        client.write_table(
            run,
            "measurements",
            pa.table({"value": values}),
            storage_mode="auto",
            rows_per_run_estimate=10000,
            run_count_estimate=5,
        )
        client.commit_run(run)
        run_ids.append(run.run_id)

    latest_run = client.open_run("measurement_pipeline", run_ids[-1])
    table_root = latest_run.session_path / "tables" / "measurements"
    assert not (table_root / "_session_compacted").exists()
    assert len(list(table_root.glob("run_id=*"))) >= 3

    manifest = client.load_manifest("measurement_pipeline", run_ids[-1])
    assert manifest.tables["measurements"].locked_storage_mode == "partitioned_by_run"

    session_table = client.read_table(
        "measurement_pipeline",
        run_ids[-1],
        "measurements",
        read_scope="session",
    )
    assert session_table.num_rows == 3000

    run_table = client.read_table("measurement_pipeline", run_ids[0], "measurements")
    assert run_table.num_rows == 1000
