"""Tests for claude_code.state — session state persistence."""
from __future__ import annotations

import json
import time

import pytest

from staso.claude_code.state import (
    HookState,
    acquire_lock,
    delete_state,
    load_state,
    release_lock,
    save_state,
)


class TestHookState:
    def test_roundtrip(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)

        state = HookState(
            session_id="sess-1",
            cwd="/home/user/project",
            turn_count=3,
            current_trace_id="trace-abc",
            current_turn_span_id="span-xyz",
            current_turn_start=1234567890.0,
            current_turn_prompt="Hello",
            last_transcript_line=42,
        )
        save_state(state)
        loaded = load_state("sess-1")

        assert loaded is not None
        assert loaded.session_id == "sess-1"
        assert loaded.cwd == "/home/user/project"
        assert loaded.turn_count == 3
        assert loaded.current_trace_id == "trace-abc"
        assert loaded.current_turn_span_id == "span-xyz"
        assert loaded.current_turn_start == 1234567890.0
        assert loaded.current_turn_prompt == "Hello"
        assert loaded.last_transcript_line == 42

    def test_load_missing_returns_none(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        assert load_state("nonexistent") is None

    def test_load_corrupted_returns_none(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        (tmp_path / "sess-bad.json").write_text("not json")
        assert load_state("sess-bad") is None

    def test_delete_state(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        save_state(HookState(session_id="s1", cwd="/"))
        assert load_state("s1") is not None
        delete_state("s1")
        assert load_state("s1") is None

    def test_delete_missing_is_noop(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        delete_state("no-such-session")  # must not raise

    def test_defaults(self):
        state = HookState(session_id="s", cwd="")
        assert state.turn_count == 0
        assert state.current_trace_id is None
        assert state.current_turn_span_id is None
        assert state.last_transcript_line == 0

    def test_from_dict_ignores_unknown_keys(self):
        data = {
            "session_id": "s1",
            "cwd": "/",
            "turn_count": 1,
            "unknown_future_field": "value",
        }
        state = HookState.from_dict(data)
        assert state.session_id == "s1"
        assert state.turn_count == 1


class TestSessionIdValidation:
    def test_rejects_path_traversal(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        with pytest.raises(ValueError, match="Invalid session_id"):
            save_state(HookState(session_id="../../etc/evil", cwd="/"))

    def test_rejects_slashes(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        with pytest.raises(ValueError, match="Invalid session_id"):
            load_state("foo/bar")

    def test_rejects_empty(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        with pytest.raises(ValueError, match="Invalid session_id"):
            load_state("")

    def test_accepts_uuid(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        save_state(HookState(session_id="550e8400-e29b-41d4-a716-446655440000", cwd="/"))
        loaded = load_state("550e8400-e29b-41d4-a716-446655440000")
        assert loaded is not None

    def test_accepts_alphanumeric_with_hyphens_underscores(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        save_state(HookState(session_id="my_session-123", cwd="/"))
        assert load_state("my_session-123") is not None


class TestLocking:
    def test_acquire_and_release(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        acquired = acquire_lock("sess-lock")
        assert acquired is True
        release_lock("sess-lock")

    def test_double_acquire_fails(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        acquire_lock("sess-lock2")
        # With max_retries=1 and zero delay it should fail fast
        result = acquire_lock("sess-lock2", max_retries=1, retry_delay=0)
        assert result is False
        release_lock("sess-lock2")

    def test_release_nonexistent_is_safe(self, tmp_path, monkeypatch):
        monkeypatch.setattr("staso.claude_code.state._state_dir", lambda: tmp_path)
        release_lock("no-lock")  # must not raise
