"""Tests for cross-run pipeline state persistence."""

from __future__ import annotations

import json

import pytest

from pycharter.etl_generator.state import (
    FileStateStore,
    PipelineState,
    SqliteStateStore,
    create_state_store,
)


class TestPipelineState:
    """Tests for PipelineState dataclass."""

    def test_create_minimal(self):
        """PipelineState with only pipeline_name."""
        state = PipelineState(pipeline_name="test")
        assert state.pipeline_name == "test"
        assert state.watermark is None
        assert state.run_count == 0
        assert state.metadata == {}

    def test_create_full(self):
        """PipelineState with all fields."""
        state = PipelineState(
            pipeline_name="users",
            watermark="2024-01-15T10:00:00Z",
            last_run_id="abc123",
            last_run_at="2024-01-15T10:05:00Z",
            run_count=5,
            metadata={"key": "value"},
        )
        assert state.watermark == "2024-01-15T10:00:00Z"
        assert state.run_count == 5

    def test_to_dict_roundtrip(self):
        """PipelineState serializes and deserializes correctly."""
        original = PipelineState(
            pipeline_name="test",
            watermark="2024-01-01",
            last_run_id="abc",
            last_run_at="2024-01-01T00:00:00Z",
            run_count=3,
            metadata={"source": "api"},
        )
        data = original.to_dict()
        restored = PipelineState.from_dict(data)
        assert restored == original

    def test_from_dict_with_missing_optional_fields(self):
        """PipelineState.from_dict handles missing optional fields."""
        data = {"pipeline_name": "test"}
        state = PipelineState.from_dict(data)
        assert state.pipeline_name == "test"
        assert state.watermark is None
        assert state.run_count == 0

    def test_frozen(self):
        """PipelineState is immutable."""
        state = PipelineState(pipeline_name="test")
        with pytest.raises(AttributeError):
            state.pipeline_name = "changed"  # type: ignore[misc]


class TestFileStateStore:
    """Tests for FileStateStore."""

    def test_get_nonexistent_returns_none(self, tmp_path):
        """get() returns None for unknown pipeline."""
        store = FileStateStore(path=tmp_path / "state")
        assert store.get("unknown") is None

    def test_save_and_get(self, tmp_path):
        """save() persists state that get() can retrieve."""
        store = FileStateStore(path=tmp_path / "state")
        state = PipelineState(
            pipeline_name="users",
            watermark="2024-01-15",
            run_count=1,
        )
        store.save(state)

        loaded = store.get("users")
        assert loaded is not None
        assert loaded.pipeline_name == "users"
        assert loaded.watermark == "2024-01-15"
        assert loaded.run_count == 1

    def test_save_overwrites(self, tmp_path):
        """save() overwrites existing state."""
        store = FileStateStore(path=tmp_path / "state")
        store.save(PipelineState(pipeline_name="p", watermark="v1", run_count=1))
        store.save(PipelineState(pipeline_name="p", watermark="v2", run_count=2))

        loaded = store.get("p")
        assert loaded is not None
        assert loaded.watermark == "v2"
        assert loaded.run_count == 2

    def test_delete(self, tmp_path):
        """delete() removes persisted state."""
        store = FileStateStore(path=tmp_path / "state")
        store.save(PipelineState(pipeline_name="p", watermark="v1"))
        store.delete("p")
        assert store.get("p") is None

    def test_delete_nonexistent_is_noop(self, tmp_path):
        """delete() for nonexistent pipeline doesn't raise."""
        store = FileStateStore(path=tmp_path / "state")
        store.delete("nonexistent")  # should not raise

    def test_list_pipelines(self, tmp_path):
        """list_pipelines() returns names of persisted pipelines."""
        store = FileStateStore(path=tmp_path / "state")
        store.save(PipelineState(pipeline_name="alpha"))
        store.save(PipelineState(pipeline_name="beta"))
        store.save(PipelineState(pipeline_name="gamma"))

        names = store.list_pipelines()
        assert names == ["alpha", "beta", "gamma"]

    def test_list_pipelines_empty_dir(self, tmp_path):
        """list_pipelines() returns empty list when no state files exist."""
        store = FileStateStore(path=tmp_path / "state")
        assert store.list_pipelines() == []

    def test_creates_directory(self, tmp_path):
        """save() creates the state directory if it doesn't exist."""
        state_dir = tmp_path / "nested" / "state"
        store = FileStateStore(path=state_dir)
        store.save(PipelineState(pipeline_name="test"))
        assert state_dir.exists()

    def test_state_file_is_valid_json(self, tmp_path):
        """Saved state file is valid JSON."""
        store = FileStateStore(path=tmp_path / "state")
        store.save(PipelineState(pipeline_name="test", watermark="2024-01-01"))
        content = (tmp_path / "state" / "test.json").read_text()
        data = json.loads(content)
        assert data["pipeline_name"] == "test"


class TestSqliteStateStore:
    """Tests for SqliteStateStore."""

    def test_get_nonexistent_returns_none(self, tmp_path):
        """get() returns None for unknown pipeline."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        assert store.get("unknown") is None

    def test_save_and_get(self, tmp_path):
        """save() persists state that get() can retrieve."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        state = PipelineState(
            pipeline_name="users",
            watermark="2024-01-15",
            run_count=1,
            metadata={"key": "value"},
        )
        store.save(state)

        loaded = store.get("users")
        assert loaded is not None
        assert loaded.pipeline_name == "users"
        assert loaded.watermark == "2024-01-15"
        assert loaded.run_count == 1
        assert loaded.metadata == {"key": "value"}

    def test_save_overwrites(self, tmp_path):
        """save() overwrites existing state (INSERT OR REPLACE)."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        store.save(PipelineState(pipeline_name="p", watermark="v1", run_count=1))
        store.save(PipelineState(pipeline_name="p", watermark="v2", run_count=2))

        loaded = store.get("p")
        assert loaded is not None
        assert loaded.watermark == "v2"
        assert loaded.run_count == 2

    def test_delete(self, tmp_path):
        """delete() removes persisted state."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        store.save(PipelineState(pipeline_name="p", watermark="v1"))
        store.delete("p")
        assert store.get("p") is None

    def test_delete_nonexistent_is_noop(self, tmp_path):
        """delete() for nonexistent pipeline doesn't raise."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        store.delete("nonexistent")

    def test_list_pipelines(self, tmp_path):
        """list_pipelines() returns names of persisted pipelines."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        store.save(PipelineState(pipeline_name="alpha"))
        store.save(PipelineState(pipeline_name="beta"))
        store.save(PipelineState(pipeline_name="gamma"))

        names = store.list_pipelines()
        assert names == ["alpha", "beta", "gamma"]

    def test_list_pipelines_empty(self, tmp_path):
        """list_pipelines() returns empty list when no state exists."""
        store = SqliteStateStore(path=tmp_path / "state.db")
        assert store.list_pipelines() == []

    def test_creates_parent_directory(self, tmp_path):
        """SqliteStateStore creates parent directory for the DB file."""
        db_path = tmp_path / "nested" / "dir" / "state.db"
        store = SqliteStateStore(path=db_path)
        store.save(PipelineState(pipeline_name="test"))
        assert db_path.exists()


class TestCreateStateStore:
    """Tests for the create_state_store factory function."""

    def test_default_is_file(self, tmp_path):
        """Default backend is FileStateStore."""
        store = create_state_store({"path": str(tmp_path / "state")})
        assert isinstance(store, FileStateStore)

    def test_explicit_file_backend(self, tmp_path):
        """backend='file' creates FileStateStore."""
        store = create_state_store(
            {
                "backend": "file",
                "path": str(tmp_path / "state"),
            }
        )
        assert isinstance(store, FileStateStore)

    def test_sqlite_backend(self, tmp_path):
        """backend='sqlite' creates SqliteStateStore."""
        store = create_state_store(
            {
                "backend": "sqlite",
                "path": str(tmp_path / "state.db"),
            }
        )
        assert isinstance(store, SqliteStateStore)

    def test_unknown_backend_raises(self):
        """Unknown backend raises ValueError."""
        with pytest.raises(ValueError, match="Unknown state backend"):
            create_state_store({"backend": "redis"})

    def test_empty_config_defaults(self, tmp_path):
        """Empty config creates FileStateStore with default path."""
        store = create_state_store({})
        assert isinstance(store, FileStateStore)
