"""Integration tests for HANASaver against a real SAP HANA Cloud instance.

Usage:
    export HANA_HOST="your-instance.hanacloud.ondemand.com"
    export HANA_PORT=443
    export HANA_USER="DBADMIN"
    export HANA_PASSWORD="your-password"
    pytest tests/test_integration.py -v

These tests create and drop their own tables (prefixed LANGGRAPH_TEST_*)
so they're safe to run against a shared instance.

Skip all tests if HANA_HOST is not set:
    pytest tests/test_integration.py  →  all skipped
"""

from __future__ import annotations

import os
import uuid

import pytest

# Skip entire module if no HANA connection available
pytestmark = pytest.mark.skipif(
    not os.environ.get("HANA_HOST"),
    reason="HANA_HOST not set — skipping integration tests",
)

from langgraph_checkpoint_hana.saver import HANASaver


# ── Fixtures ─────────────────────────────────────────────────────────


@pytest.fixture(scope="module")
def saver():
    """Create a HANASaver connected to a real HANA instance.

    Uses a unique table prefix per test run to avoid collisions.
    Tables are dropped after all tests complete.
    """
    import langgraph_checkpoint_hana.saver as saver_module

    # Use unique table names so parallel runs don't collide
    run_id = uuid.uuid4().hex[:8]
    original_cp_table = saver_module._CHECKPOINTS_TABLE
    original_wr_table = saver_module._WRITES_TABLE
    saver_module._CHECKPOINTS_TABLE = f"LANGGRAPH_TEST_CP_{run_id}"
    saver_module._WRITES_TABLE = f"LANGGRAPH_TEST_WR_{run_id}"

    # Rebuild SQL templates with new table names
    saver_module._SELECT_CHECKPOINT = saver_module._SELECT_CHECKPOINT.replace(
        original_cp_table, saver_module._CHECKPOINTS_TABLE
    )
    saver_module._UPSERT_CHECKPOINT = saver_module._UPSERT_CHECKPOINT.replace(
        original_cp_table, saver_module._CHECKPOINTS_TABLE
    )
    saver_module._UPSERT_WRITE = saver_module._UPSERT_WRITE.replace(
        original_wr_table, saver_module._WRITES_TABLE
    )
    saver_module._CREATE_CHECKPOINTS = saver_module._CREATE_CHECKPOINTS.replace(
        original_cp_table, saver_module._CHECKPOINTS_TABLE
    )
    saver_module._CREATE_WRITES = saver_module._CREATE_WRITES.replace(
        original_wr_table, saver_module._WRITES_TABLE
    )

    s = HANASaver.from_env()
    s.setup()
    yield s

    # Cleanup: drop test tables and restore original module state
    test_cp_table = saver_module._CHECKPOINTS_TABLE
    test_wr_table = saver_module._WRITES_TABLE
    cur = s.conn.cursor()
    try:
        cur.execute(f'DROP TABLE "{test_cp_table}"')
        cur.execute(f'DROP TABLE "{test_wr_table}"')
        s.conn.commit()
    except Exception:
        pass
    finally:
        cur.close()
        s.conn.close()

    # Restore original module-level variables
    saver_module._CHECKPOINTS_TABLE = original_cp_table
    saver_module._WRITES_TABLE = original_wr_table
    saver_module._SELECT_CHECKPOINT = saver_module._SELECT_CHECKPOINT.replace(
        test_cp_table, original_cp_table
    )
    saver_module._UPSERT_CHECKPOINT = saver_module._UPSERT_CHECKPOINT.replace(
        test_cp_table, original_cp_table
    )
    saver_module._UPSERT_WRITE = saver_module._UPSERT_WRITE.replace(
        test_wr_table, original_wr_table
    )
    saver_module._CREATE_CHECKPOINTS = saver_module._CREATE_CHECKPOINTS.replace(
        test_cp_table, original_cp_table
    )
    saver_module._CREATE_WRITES = saver_module._CREATE_WRITES.replace(
        test_wr_table, original_wr_table
    )


@pytest.fixture
def thread_id():
    """Unique thread ID per test."""
    return f"test-thread-{uuid.uuid4().hex[:12]}"


# ── Tests ────────────────────────────────────────────────────────────


class TestConnection:

    def test_connection_is_alive(self, saver):
        cur = saver.conn.cursor()
        cur.execute("SELECT 1 FROM DUMMY")
        assert cur.fetchone()[0] == 1
        cur.close()

    def test_tables_exist(self, saver):
        import langgraph_checkpoint_hana.saver as saver_module

        cur = saver.conn.cursor()
        cur.execute(
            'SELECT COUNT(*) FROM "SYS"."TABLES" WHERE TABLE_NAME = ?',
            (saver_module._CHECKPOINTS_TABLE,),
        )
        assert cur.fetchone()[0] == 1
        cur.execute(
            'SELECT COUNT(*) FROM "SYS"."TABLES" WHERE TABLE_NAME = ?',
            (saver_module._WRITES_TABLE,),
        )
        assert cur.fetchone()[0] == 1
        cur.close()


class TestRoundTrip:
    """Put a checkpoint and get it back — the essential test."""

    def test_put_and_get(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        # Create a minimal valid checkpoint
        checkpoint = {
            "v": 1,
            "id": str(uuid.uuid4()),
            "ts": "2026-02-23T12:00:00+00:00",
            "channel_values": {"messages": [{"role": "user", "content": "hello"}]},
            "channel_versions": {"__start__": "1"},
            "versions_seen": {},
            "pending_sends": [],
        }
        metadata = {"source": "input", "step": 0}

        # Put
        result_config = saver.put(config, checkpoint, metadata, {})
        assert result_config["configurable"]["checkpoint_id"] == checkpoint["id"]

        # Get
        result = saver.get_tuple(config)
        assert result is not None
        assert result.checkpoint["id"] == checkpoint["id"]
        assert result.checkpoint["channel_values"]["messages"][0]["content"] == "hello"
        assert result.metadata["source"] == "input"
        assert result.parent_config is None

    def test_multiple_checkpoints_returns_latest(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        ids = []
        for i in range(3):
            cp_id = f"{i:032d}"  # Lexicographically ordered
            checkpoint = {
                "v": 1,
                "id": cp_id,
                "ts": f"2026-02-23T12:0{i}:00+00:00",
                "channel_values": {"step": i},
                "channel_versions": {},
                "versions_seen": {},
                "pending_sends": [],
            }
            parent_config = (
                {
                    "configurable": {
                        "thread_id": thread_id,
                        "checkpoint_ns": "",
                        "checkpoint_id": ids[-1],
                    }
                }
                if ids
                else config
            )
            saver.put(parent_config, checkpoint, {"step": i}, {})
            ids.append(cp_id)

        # get_tuple without checkpoint_id returns the latest
        result = saver.get_tuple(config)
        assert result is not None
        assert result.checkpoint["id"] == ids[-1]

    def test_get_specific_checkpoint(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        first_id = str(uuid.uuid4())
        second_id = str(uuid.uuid4())

        for cp_id in [first_id, second_id]:
            checkpoint = {
                "v": 1,
                "id": cp_id,
                "ts": "2026-02-23T12:00:00+00:00",
                "channel_values": {"which": cp_id},
                "channel_versions": {},
                "versions_seen": {},
                "pending_sends": [],
            }
            saver.put(config, checkpoint, {}, {})

        # Fetch the first one specifically
        specific_config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
                "checkpoint_id": first_id,
            }
        }
        result = saver.get_tuple(specific_config)
        assert result is not None
        assert result.checkpoint["channel_values"]["which"] == first_id


class TestWrites:

    def test_put_and_retrieve_writes(self, saver, thread_id):
        cp_id = str(uuid.uuid4())
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
                "checkpoint_id": cp_id,
            }
        }

        # Store a checkpoint first
        checkpoint = {
            "v": 1,
            "id": cp_id,
            "ts": "2026-02-23T12:00:00+00:00",
            "channel_values": {},
            "channel_versions": {},
            "versions_seen": {},
            "pending_sends": [],
        }
        saver.put(
            {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}},
            checkpoint,
            {},
            {},
        )

        # Store writes
        writes = [
            ("messages", {"role": "assistant", "content": "Hi there!"}),
            ("next_step", "end"),
        ]
        saver.put_writes(config, writes, task_id="task-abc")

        # Retrieve and check pending writes
        result = saver.get_tuple(
            {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
        )
        assert result is not None
        assert len(result.pending_writes) == 2
        assert result.pending_writes[0][1] == "messages"  # channel
        assert result.pending_writes[0][2]["content"] == "Hi there!"


class TestList:

    def test_list_checkpoints(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        for i in range(5):
            checkpoint = {
                "v": 1,
                "id": f"{i:032d}",
                "ts": f"2026-02-23T12:0{i}:00+00:00",
                "channel_values": {"step": i},
                "channel_versions": {},
                "versions_seen": {},
                "pending_sends": [],
            }
            saver.put(config, checkpoint, {"step": i}, {})

        results = list(saver.list(config))
        assert len(results) == 5
        # Should be newest first
        assert results[0].checkpoint["channel_values"]["step"] == 4

    def test_list_with_limit(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        for i in range(5):
            checkpoint = {
                "v": 1,
                "id": f"{i:032d}",
                "ts": f"2026-02-23T12:0{i}:00+00:00",
                "channel_values": {},
                "channel_versions": {},
                "versions_seen": {},
                "pending_sends": [],
            }
            saver.put(config, checkpoint, {}, {})

        results = list(saver.list(config, limit=2))
        assert len(results) == 2

    def test_list_with_metadata_filter(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        for i in range(3):
            checkpoint = {
                "v": 1,
                "id": f"{i:032d}",
                "ts": f"2026-02-23T12:0{i}:00+00:00",
                "channel_values": {},
                "channel_versions": {},
                "versions_seen": {},
                "pending_sends": [],
            }
            source = "input" if i % 2 == 0 else "loop"
            saver.put(config, checkpoint, {"source": source}, {})

        results = list(saver.list(config, filter={"source": "input"}))
        assert len(results) == 2
        assert all(r.metadata["source"] == "input" for r in results)


class TestDeleteThread:

    def test_delete_removes_everything(self, saver, thread_id):
        config = {
            "configurable": {
                "thread_id": thread_id,
                "checkpoint_ns": "",
            }
        }

        cp_id = str(uuid.uuid4())
        checkpoint = {
            "v": 1,
            "id": cp_id,
            "ts": "2026-02-23T12:00:00+00:00",
            "channel_values": {"data": "will be deleted"},
            "channel_versions": {},
            "versions_seen": {},
            "pending_sends": [],
        }
        saver.put(config, checkpoint, {}, {})
        saver.put_writes(
            {**config, "configurable": {**config["configurable"], "checkpoint_id": cp_id}},
            [("messages", "test")],
            task_id="t1",
        )

        # Verify it exists
        assert saver.get_tuple(config) is not None

        # Delete
        saver.delete_thread(thread_id)

        # Verify it's gone
        assert saver.get_tuple(config) is None
        assert list(saver.list(config)) == []


class TestWithLangGraph:
    """End-to-end test with an actual LangGraph graph.

    This is the real proof — compile a graph with the checkpointer
    and verify multi-turn conversation memory works.

    Requires: pip install langgraph langchain-core
    """

    def test_graph_conversation_memory(self, saver, thread_id):
        """Two invocations on the same thread should share state."""
        try:
            from langgraph.graph import StateGraph, MessagesState, START, END
        except ImportError:
            pytest.skip("langgraph not installed")

        # Minimal echo graph
        def echo(state: MessagesState) -> MessagesState:
            last_msg = state["messages"][-1]
            return {"messages": [{"role": "assistant", "content": f"Echo: {last_msg.content}"}]}

        workflow = StateGraph(MessagesState)
        workflow.add_node("echo", echo)
        workflow.add_edge(START, "echo")
        workflow.add_edge("echo", END)
        graph = workflow.compile(checkpointer=saver)

        config = {"configurable": {"thread_id": thread_id}}

        # First turn
        result1 = graph.invoke(
            {"messages": [{"role": "user", "content": "hello"}]},
            config,
        )
        assert any("Echo: hello" in str(m) for m in result1["messages"])

        # Second turn — should see previous messages
        result2 = graph.invoke(
            {"messages": [{"role": "user", "content": "world"}]},
            config,
        )
        messages = result2["messages"]
        # Should have 4 messages: user1, assistant1, user2, assistant2
        assert len(messages) == 4
        assert any("Echo: world" in str(m) for m in messages)

    def test_separate_threads_isolated(self, saver):
        """Different thread_ids should not share state."""
        try:
            from langgraph.graph import StateGraph, MessagesState, START, END
        except ImportError:
            pytest.skip("langgraph not installed")

        def echo(state: MessagesState) -> MessagesState:
            return {"messages": [{"role": "assistant", "content": f"count: {len(state['messages'])}"}]}

        workflow = StateGraph(MessagesState)
        workflow.add_node("echo", echo)
        workflow.add_edge(START, "echo")
        workflow.add_edge("echo", END)
        graph = workflow.compile(checkpointer=saver)

        thread_a = f"thread-a-{uuid.uuid4().hex[:8]}"
        thread_b = f"thread-b-{uuid.uuid4().hex[:8]}"

        # 3 turns on thread A
        for i in range(3):
            graph.invoke(
                {"messages": [{"role": "user", "content": f"msg {i}"}]},
                {"configurable": {"thread_id": thread_a}},
            )

        # 1 turn on thread B
        result_b = graph.invoke(
            {"messages": [{"role": "user", "content": "first msg"}]},
            {"configurable": {"thread_id": thread_b}},
        )

        # Thread B should only see its own messages (user + assistant = 2)
        assert len(result_b["messages"]) == 2
