use std::{
    collections::BTreeMap,
    path::Path,
    sync::{Mutex, MutexGuard},
    time::Duration,
};

use nana_domain::{RuntimeState, StoryNode, stable_json_hash};
use rusqlite::{Connection, OptionalExtension, TransactionBehavior, params};
use thiserror::Error;

const SCHEMA_VERSION: i64 = 1;
#[cfg(test)]
const BUSY_TIMEOUT_MILLIS: i64 = 5_000;
const BUSY_TIMEOUT: Duration = Duration::from_secs(5);

#[derive(Debug, Error, PartialEq, Eq)]
pub enum StoreError {
    #[error("story not found: {0}")]
    StoryNotFound(String),
    #[error("branch not found: {story_id}/{branch_id}")]
    BranchNotFound { story_id: String, branch_id: String },
    #[error("node already exists: {0}")]
    NodeAlreadyExists(String),
    #[error("parent node not found: {0}")]
    ParentNotFound(String),
    #[error("branch head changed: expected {expected}, got {actual}")]
    StaleBranchHead { expected: String, actual: String },
    #[error("node and materialized state disagree: {0}")]
    StateMismatch(&'static str),
    #[error("materialized state hash does not match node: {node_id}")]
    StateHashMismatch { node_id: String },
    #[error("sqlite storage error: {0}")]
    Sqlite(String),
    #[error("JSON serialization error: {0}")]
    Serialization(String),
    #[error("storage lock is poisoned")]
    Poisoned,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum ForkError {
    #[error("branch already exists: {story_id}/{branch_id}")]
    BranchAlreadyExists { story_id: String, branch_id: String },
    #[error("invalid branch id: {0}")]
    InvalidBranchId(String),
    #[error(transparent)]
    Store(#[from] StoreError),
}

impl From<rusqlite::Error> for ForkError {
    fn from(error: rusqlite::Error) -> Self {
        Self::Store(StoreError::from(error))
    }
}

impl From<rusqlite::Error> for StoreError {
    fn from(error: rusqlite::Error) -> Self {
        Self::Sqlite(error.to_string())
    }
}

impl From<serde_json::Error> for StoreError {
    fn from(error: serde_json::Error) -> Self {
        Self::Serialization(error.to_string())
    }
}

pub trait StoryStore: Send + Sync {
    fn append_node(&self, node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError>;

    /// Creates a branch whose initial head is an existing immutable node.
    ///
    /// The source node and its materialized state are not copied or changed.
    /// The returned state is positioned on `new_branch_id`, ready for the next
    /// append to use `source_node_id` as its parent. A new branch identifier is
    /// 1–128 ASCII bytes containing only letters, numbers, `.`, `_`, or `-`.
    fn fork_branch(
        &self,
        story_id: &str,
        source_node_id: &str,
        new_branch_id: &str,
    ) -> Result<RuntimeState, ForkError>;

    fn load_state(&self, story_id: &str, branch_id: &str) -> Result<RuntimeState, StoreError>;

    fn load_node(&self, story_id: &str, node_id: &str) -> Result<StoryNode, StoreError>;
}

#[derive(Debug, Default)]
struct MemoryData {
    nodes: BTreeMap<(String, String), StoryNode>,
    states: BTreeMap<(String, String), RuntimeState>,
    branch_heads: BTreeMap<(String, String), String>,
}

/// Deterministic test and development store.
///
/// Production persistence will implement the same append-only semantics with
/// `SQLite`. Keeping this implementation in the crate lets the engine/runtime
/// integration tests exercise branch rules without depending on a database.
#[derive(Debug, Default)]
pub struct InMemoryStoryStore {
    data: Mutex<MemoryData>,
}

impl InMemoryStoryStore {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn branch_head(
        &self,
        story_id: &str,
        branch_id: &str,
    ) -> Result<Option<String>, StoreError> {
        let data = self.lock()?;
        Ok(data
            .branch_heads
            .get(&(story_id.to_owned(), branch_id.to_owned()))
            .cloned())
    }

    pub fn load_state_at_node(
        &self,
        story_id: &str,
        node_id: &str,
    ) -> Result<RuntimeState, StoreError> {
        let data = self.lock()?;
        data.states
            .get(&(story_id.to_owned(), node_id.to_owned()))
            .cloned()
            .ok_or_else(|| StoreError::ParentNotFound(node_id.to_owned()))
    }

    fn lock(&self) -> Result<MutexGuard<'_, MemoryData>, StoreError> {
        self.data.lock().map_err(|_| StoreError::Poisoned)
    }
}

impl StoryStore for InMemoryStoryStore {
    fn append_node(&self, node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError> {
        validate_materialized_state(node, state)?;

        let node_key = (node.story_id.clone(), node.id.clone());
        let branch_key = (node.story_id.clone(), node.branch_id.clone());
        let mut data = self.lock()?;

        if data.nodes.contains_key(&node_key) {
            return Err(StoreError::NodeAlreadyExists(node.id.clone()));
        }

        if let Some(parent_id) = &node.parent_id {
            if !data
                .nodes
                .contains_key(&(node.story_id.clone(), parent_id.clone()))
            {
                return Err(StoreError::ParentNotFound(parent_id.clone()));
            }
        } else if data
            .nodes
            .keys()
            .any(|(story_id, _)| story_id == &node.story_id)
        {
            return Err(StoreError::ParentNotFound(
                "a non-root node must name its parent".to_owned(),
            ));
        }

        if let Some(current_head) = data.branch_heads.get(&branch_key) {
            let expected_parent = node.parent_id.as_deref().unwrap_or("<none>");
            if current_head != expected_parent {
                return Err(StoreError::StaleBranchHead {
                    expected: current_head.clone(),
                    actual: expected_parent.to_owned(),
                });
            }
        }

        // All validation happens before any map is changed. This mirrors the
        // all-or-nothing transaction boundary required from the SQLite store.
        data.nodes.insert(node_key.clone(), node.clone());
        data.states.insert(node_key, state.clone());
        data.branch_heads.insert(branch_key, node.id.clone());
        Ok(())
    }

    fn fork_branch(
        &self,
        story_id: &str,
        source_node_id: &str,
        new_branch_id: &str,
    ) -> Result<RuntimeState, ForkError> {
        validate_new_branch_id(new_branch_id)?;

        let branch_key = (story_id.to_owned(), new_branch_id.to_owned());
        let source_key = (story_id.to_owned(), source_node_id.to_owned());
        let mut data = self.lock()?;

        if data.branch_heads.contains_key(&branch_key) {
            return Err(ForkError::BranchAlreadyExists {
                story_id: story_id.to_owned(),
                branch_id: new_branch_id.to_owned(),
            });
        }

        let node = data
            .nodes
            .get(&source_key)
            .ok_or_else(|| StoreError::ParentNotFound(source_node_id.to_owned()))?;
        let state = data
            .states
            .get(&source_key)
            .ok_or(StoreError::StateMismatch("node has no materialized state"))?;
        let restored =
            restore_state_for_branch(node, state, story_id, source_node_id, new_branch_id)?;

        // Validation is complete before the only mutation.
        data.branch_heads
            .insert(branch_key, source_node_id.to_owned());
        Ok(restored)
    }

    fn load_state(&self, story_id: &str, branch_id: &str) -> Result<RuntimeState, StoreError> {
        let data = self.lock()?;
        let story_exists = data
            .nodes
            .keys()
            .any(|(stored_story_id, _)| stored_story_id == story_id);
        if !story_exists {
            return Err(StoreError::StoryNotFound(story_id.to_owned()));
        }

        let head = data
            .branch_heads
            .get(&(story_id.to_owned(), branch_id.to_owned()))
            .ok_or_else(|| StoreError::BranchNotFound {
                story_id: story_id.to_owned(),
                branch_id: branch_id.to_owned(),
            })?;
        let node = data
            .nodes
            .get(&(story_id.to_owned(), head.clone()))
            .ok_or(StoreError::StateMismatch("branch head has no node"))?;
        let state = data
            .states
            .get(&(story_id.to_owned(), head.clone()))
            .ok_or(StoreError::StateMismatch("branch head has no state"))?;
        restore_state_for_branch(node, state, story_id, head, branch_id)
    }

    fn load_node(&self, story_id: &str, node_id: &str) -> Result<StoryNode, StoreError> {
        let data = self.lock()?;
        data.nodes
            .get(&(story_id.to_owned(), node_id.to_owned()))
            .cloned()
            .ok_or_else(|| StoreError::ParentNotFound(node_id.to_owned()))
    }
}

/// Durable `SQLite` implementation of the append-only story store.
///
/// A connection is kept behind a mutex so one store value preserves the same
/// `Send + Sync` contract as the in-memory implementation. `SQLite` still
/// provides the cross-connection transaction boundary when a database is
/// reopened or shared by multiple store values.
pub struct SqliteStoryStore {
    connection: Mutex<Connection>,
}

#[derive(Clone, Copy)]
enum DatabaseKind {
    File,
    InMemory,
}

impl SqliteStoryStore {
    /// Opens or creates a story database at `path`.
    pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
        let connection = Connection::open(path)?;
        Self::from_connection(connection, DatabaseKind::File)
    }

    /// Opens a fresh in-memory story database.
    pub fn open_in_memory() -> Result<Self, StoreError> {
        let connection = Connection::open_in_memory()?;
        Self::from_connection(connection, DatabaseKind::InMemory)
    }

    /// Returns the current head node for a branch, if the branch exists.
    pub fn branch_head(
        &self,
        story_id: &str,
        branch_id: &str,
    ) -> Result<Option<String>, StoreError> {
        let connection = self.lock()?;
        connection
            .query_row(
                "SELECT head_node_id
                 FROM branch_heads
                 WHERE story_id = ?1 AND branch_id = ?2",
                params![story_id, branch_id],
                |row| row.get(0),
            )
            .optional()
            .map_err(StoreError::from)
    }

    /// Loads the materialized state associated with a specific node.
    pub fn load_state_at_node(
        &self,
        story_id: &str,
        node_id: &str,
    ) -> Result<RuntimeState, StoreError> {
        let connection = self.lock()?;
        let stored = connection
            .query_row(
                "SELECT nodes.branch_id, nodes.parent_id, nodes.node_json,
                        materialized_states.state_json
                 FROM nodes
                 LEFT JOIN materialized_states
                   ON materialized_states.story_id = nodes.story_id
                  AND materialized_states.node_id = nodes.node_id
                 WHERE nodes.story_id = ?1 AND nodes.node_id = ?2",
                params![story_id, node_id],
                |row| {
                    Ok((
                        row.get::<_, String>(0)?,
                        row.get::<_, Option<String>>(1)?,
                        row.get::<_, String>(2)?,
                        row.get::<_, Option<String>>(3)?,
                    ))
                },
            )
            .optional()?
            .ok_or_else(|| StoreError::ParentNotFound(node_id.to_owned()))?;

        let node = deserialize_node(&stored.2)?;
        validate_loaded_node(&node, story_id, node_id, &stored.0, stored.1.as_deref())?;
        let state_json = stored
            .3
            .ok_or(StoreError::StateMismatch("node has no materialized state"))?;
        let state = deserialize_state(&state_json)?;
        validate_loaded_state(&state, story_id, node_id, &stored.0)?;
        validate_state_hash(&node, &state)?;
        Ok(state)
    }

    fn from_connection(
        mut connection: Connection,
        database_kind: DatabaseKind,
    ) -> Result<Self, StoreError> {
        configure_connection(&connection)?;
        initialize_schema(&mut connection)?;
        configure_journal(&connection, database_kind)?;
        Ok(Self {
            connection: Mutex::new(connection),
        })
    }

    fn lock(&self) -> Result<MutexGuard<'_, Connection>, StoreError> {
        self.connection.lock().map_err(|_| StoreError::Poisoned)
    }
}

impl StoryStore for SqliteStoryStore {
    fn append_node(&self, node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError> {
        validate_materialized_state(node, state)?;
        let node_json = serde_json::to_string(node)?;
        let state_json = serde_json::to_string(state)?;

        let mut connection = self.lock()?;
        let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;

        let node_exists = transaction.query_row(
            "SELECT EXISTS(
                SELECT 1 FROM nodes WHERE story_id = ?1 AND node_id = ?2
             )",
            params![node.story_id, node.id],
            |row| row.get::<_, bool>(0),
        )?;
        if node_exists {
            return Err(StoreError::NodeAlreadyExists(node.id.clone()));
        }

        if let Some(parent_id) = &node.parent_id {
            let parent_exists = transaction.query_row(
                "SELECT EXISTS(
                    SELECT 1 FROM nodes WHERE story_id = ?1 AND node_id = ?2
                 )",
                params![node.story_id, parent_id],
                |row| row.get::<_, bool>(0),
            )?;
            if !parent_exists {
                return Err(StoreError::ParentNotFound(parent_id.clone()));
            }
        } else {
            let story_exists = transaction.query_row(
                "SELECT EXISTS(SELECT 1 FROM nodes WHERE story_id = ?1)",
                params![node.story_id],
                |row| row.get::<_, bool>(0),
            )?;
            if story_exists {
                return Err(StoreError::ParentNotFound(
                    "a non-root node must name its parent".to_owned(),
                ));
            }
        }

        let current_head = transaction
            .query_row(
                "SELECT head_node_id
                 FROM branch_heads
                 WHERE story_id = ?1 AND branch_id = ?2",
                params![node.story_id, node.branch_id],
                |row| row.get::<_, String>(0),
            )
            .optional()?;
        if let Some(current_head) = current_head {
            let expected_parent = node.parent_id.as_deref().unwrap_or("<none>");
            if current_head != expected_parent {
                return Err(StoreError::StaleBranchHead {
                    expected: current_head,
                    actual: expected_parent.to_owned(),
                });
            }
        }

        transaction.execute(
            "INSERT INTO nodes (
                story_id, node_id, branch_id, parent_id, node_json
             ) VALUES (?1, ?2, ?3, ?4, ?5)",
            params![
                node.story_id,
                node.id,
                node.branch_id,
                node.parent_id,
                node_json
            ],
        )?;
        transaction.execute(
            "INSERT INTO materialized_states (story_id, node_id, state_json)
             VALUES (?1, ?2, ?3)",
            params![node.story_id, node.id, state_json],
        )?;
        transaction.execute(
            "INSERT INTO branch_heads (story_id, branch_id, head_node_id)
             VALUES (?1, ?2, ?3)
             ON CONFLICT(story_id, branch_id)
             DO UPDATE SET head_node_id = excluded.head_node_id",
            params![node.story_id, node.branch_id, node.id],
        )?;
        transaction.commit()?;
        Ok(())
    }

    fn fork_branch(
        &self,
        story_id: &str,
        source_node_id: &str,
        new_branch_id: &str,
    ) -> Result<RuntimeState, ForkError> {
        validate_new_branch_id(new_branch_id)?;

        let mut connection = self.lock()?;
        let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;

        let branch_exists = transaction.query_row(
            "SELECT EXISTS(
                 SELECT 1 FROM branch_heads
                 WHERE story_id = ?1 AND branch_id = ?2
             )",
            params![story_id, new_branch_id],
            |row| row.get::<_, bool>(0),
        )?;
        if branch_exists {
            return Err(ForkError::BranchAlreadyExists {
                story_id: story_id.to_owned(),
                branch_id: new_branch_id.to_owned(),
            });
        }

        let stored = transaction
            .query_row(
                "SELECT nodes.branch_id, nodes.parent_id, nodes.node_json,
                        materialized_states.state_json
                 FROM nodes
                 LEFT JOIN materialized_states
                   ON materialized_states.story_id = nodes.story_id
                  AND materialized_states.node_id = nodes.node_id
                 WHERE nodes.story_id = ?1 AND nodes.node_id = ?2",
                params![story_id, source_node_id],
                |row| {
                    Ok((
                        row.get::<_, String>(0)?,
                        row.get::<_, Option<String>>(1)?,
                        row.get::<_, String>(2)?,
                        row.get::<_, Option<String>>(3)?,
                    ))
                },
            )
            .optional()?
            .ok_or_else(|| StoreError::ParentNotFound(source_node_id.to_owned()))?;

        let node = deserialize_node(&stored.2)?;
        validate_loaded_node(
            &node,
            story_id,
            source_node_id,
            &stored.0,
            stored.1.as_deref(),
        )?;
        let state_json = stored
            .3
            .ok_or(StoreError::StateMismatch("node has no materialized state"))?;
        let state = deserialize_state(&state_json)?;
        let restored =
            restore_state_for_branch(&node, &state, story_id, source_node_id, new_branch_id)?;

        transaction.execute(
            "INSERT INTO branch_heads (story_id, branch_id, head_node_id)
             VALUES (?1, ?2, ?3)",
            params![story_id, new_branch_id, source_node_id],
        )?;
        transaction.commit()?;
        Ok(restored)
    }

    fn load_state(&self, story_id: &str, branch_id: &str) -> Result<RuntimeState, StoreError> {
        let connection = self.lock()?;
        let story_exists = connection.query_row(
            "SELECT EXISTS(SELECT 1 FROM nodes WHERE story_id = ?1)",
            params![story_id],
            |row| row.get::<_, bool>(0),
        )?;
        if !story_exists {
            return Err(StoreError::StoryNotFound(story_id.to_owned()));
        }

        let stored = connection
            .query_row(
                "SELECT branch_heads.head_node_id, nodes.branch_id, nodes.parent_id,
                        nodes.node_json, materialized_states.state_json
                 FROM branch_heads
                 JOIN nodes
                   ON nodes.story_id = branch_heads.story_id
                  AND nodes.node_id = branch_heads.head_node_id
                 LEFT JOIN materialized_states
                   ON materialized_states.story_id = branch_heads.story_id
                  AND materialized_states.node_id = branch_heads.head_node_id
                 WHERE branch_heads.story_id = ?1
                   AND branch_heads.branch_id = ?2",
                params![story_id, branch_id],
                |row| {
                    Ok((
                        row.get::<_, String>(0)?,
                        row.get::<_, String>(1)?,
                        row.get::<_, Option<String>>(2)?,
                        row.get::<_, String>(3)?,
                        row.get::<_, Option<String>>(4)?,
                    ))
                },
            )
            .optional()?
            .ok_or_else(|| StoreError::BranchNotFound {
                story_id: story_id.to_owned(),
                branch_id: branch_id.to_owned(),
            })?;

        let node = deserialize_node(&stored.3)?;
        validate_loaded_node(&node, story_id, &stored.0, &stored.1, stored.2.as_deref())?;
        let state_json = stored
            .4
            .ok_or(StoreError::StateMismatch("branch head has no state"))?;
        let state = deserialize_state(&state_json)?;
        restore_state_for_branch(&node, &state, story_id, &stored.0, branch_id)
    }

    fn load_node(&self, story_id: &str, node_id: &str) -> Result<StoryNode, StoreError> {
        let connection = self.lock()?;
        let stored = connection
            .query_row(
                "SELECT branch_id, parent_id, node_json
                 FROM nodes
                 WHERE story_id = ?1 AND node_id = ?2",
                params![story_id, node_id],
                |row| {
                    Ok((
                        row.get::<_, String>(0)?,
                        row.get::<_, Option<String>>(1)?,
                        row.get::<_, String>(2)?,
                    ))
                },
            )
            .optional()?
            .ok_or_else(|| StoreError::ParentNotFound(node_id.to_owned()))?;
        let node = deserialize_node(&stored.2)?;
        validate_loaded_node(&node, story_id, node_id, &stored.0, stored.1.as_deref())?;
        Ok(node)
    }
}

fn configure_connection(connection: &Connection) -> Result<(), StoreError> {
    connection.busy_timeout(BUSY_TIMEOUT)?;
    connection.execute_batch("PRAGMA foreign_keys = ON;")?;

    let foreign_keys_enabled =
        connection.query_row("PRAGMA foreign_keys", [], |row| row.get::<_, bool>(0))?;
    if !foreign_keys_enabled {
        return Err(connection_configuration(
            "foreign key enforcement could not be enabled",
        ));
    }
    Ok(())
}

fn configure_journal(
    connection: &Connection,
    database_kind: DatabaseKind,
) -> Result<(), StoreError> {
    if matches!(database_kind, DatabaseKind::File) {
        let journal_mode = connection.query_row("PRAGMA journal_mode = WAL", [], |row| {
            row.get::<_, String>(0)
        })?;
        if !journal_mode.eq_ignore_ascii_case("wal") {
            return Err(connection_configuration(&format!(
                "file database refused WAL journal mode and selected {journal_mode}"
            )));
        }
    }

    // NORMAL preserves WAL's crash-safety guarantees while avoiding a full
    // filesystem sync on every commit. For in-memory databases this is a
    // harmless connection-local setting; WAL itself is intentionally skipped
    // because SQLite keeps their journal mode as MEMORY.
    connection.execute_batch("PRAGMA synchronous = NORMAL;")?;
    Ok(())
}

fn initialize_schema(connection: &mut Connection) -> Result<(), StoreError> {
    match schema_version(connection)? {
        SCHEMA_VERSION => validate_schema(connection, SCHEMA_VERSION),
        0 => initialize_unversioned_schema(connection),
        found => Err(unsupported_schema_version(found)),
    }
}

fn initialize_unversioned_schema(connection: &mut Connection) -> Result<(), StoreError> {
    let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
    let version = schema_version(&transaction)?;

    // Another connection may have initialized the file while this connection
    // waited for the write lock.
    if version == SCHEMA_VERSION {
        validate_schema(&transaction, SCHEMA_VERSION)?;
        transaction.commit()?;
        return Ok(());
    }
    if version != 0 {
        return Err(unsupported_schema_version(version));
    }

    if schema_has_user_objects(&transaction)? {
        // Wave 2 databases have this exact unversioned layout. Validate every
        // required table, column, foreign key, and index before adopting them;
        // a partial legacy database must never be repaired with IF NOT EXISTS.
        validate_schema(&transaction, 0)?;
    } else {
        transaction.execute_batch(
            "CREATE TABLE nodes (
                 story_id TEXT NOT NULL,
                 node_id TEXT NOT NULL,
                 branch_id TEXT NOT NULL,
                 parent_id TEXT,
                 node_json TEXT NOT NULL,
                 PRIMARY KEY (story_id, node_id),
                 FOREIGN KEY (story_id, parent_id)
                     REFERENCES nodes (story_id, node_id)
                     ON DELETE RESTRICT
             );

             CREATE UNIQUE INDEX one_root_per_story
                 ON nodes (story_id)
                 WHERE parent_id IS NULL;

             CREATE TABLE materialized_states (
                 story_id TEXT NOT NULL,
                 node_id TEXT NOT NULL,
                 state_json TEXT NOT NULL,
                 PRIMARY KEY (story_id, node_id),
                 FOREIGN KEY (story_id, node_id)
                     REFERENCES nodes (story_id, node_id)
                     ON DELETE RESTRICT
             );

             CREATE TABLE branch_heads (
                 story_id TEXT NOT NULL,
                 branch_id TEXT NOT NULL,
                 head_node_id TEXT NOT NULL,
                 PRIMARY KEY (story_id, branch_id),
                 FOREIGN KEY (story_id, head_node_id)
                     REFERENCES nodes (story_id, node_id)
                     ON DELETE RESTRICT
             );",
        )?;
        validate_schema(&transaction, SCHEMA_VERSION)?;
    }

    transaction.execute_batch(&format!("PRAGMA user_version = {SCHEMA_VERSION};"))?;
    transaction.commit()?;
    Ok(())
}

fn schema_version(connection: &Connection) -> Result<i64, StoreError> {
    connection
        .query_row("PRAGMA user_version", [], |row| row.get(0))
        .map_err(StoreError::from)
}

fn schema_has_user_objects(connection: &Connection) -> Result<bool, StoreError> {
    connection
        .query_row(
            "SELECT EXISTS(
                 SELECT 1
                 FROM sqlite_schema
                 WHERE name NOT LIKE 'sqlite_%'
                   AND type IN ('table', 'index', 'view', 'trigger')
             )",
            [],
            |row| row.get(0),
        )
        .map_err(StoreError::from)
}

#[derive(Debug, PartialEq, Eq)]
struct ColumnLayout {
    name: String,
    declared_type: String,
    not_null: bool,
    primary_key_position: i64,
}

#[derive(Debug, PartialEq, Eq)]
struct ForeignKeyLayout {
    sequence: i64,
    referenced_table: String,
    from_column: String,
    to_column: String,
    on_delete: String,
}

struct ExpectedColumn<'a> {
    name: &'a str,
    declared_type: &'a str,
    not_null: bool,
    primary_key_position: i64,
}

struct ExpectedForeignKey<'a> {
    sequence: i64,
    referenced_table: &'a str,
    from_column: &'a str,
    to_column: &'a str,
    on_delete: &'a str,
}

fn validate_schema(connection: &Connection, version: i64) -> Result<(), StoreError> {
    validate_nodes_schema(connection, version)?;
    validate_materialized_states_schema(connection, version)?;
    validate_branch_heads_schema(connection, version)?;
    validate_root_index(connection, version)?;
    Ok(())
}

fn validate_nodes_schema(connection: &Connection, version: i64) -> Result<(), StoreError> {
    validate_table(
        connection,
        version,
        "nodes",
        &[
            ExpectedColumn {
                name: "story_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 1,
            },
            ExpectedColumn {
                name: "node_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 2,
            },
            ExpectedColumn {
                name: "branch_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 0,
            },
            ExpectedColumn {
                name: "parent_id",
                declared_type: "TEXT",
                not_null: false,
                primary_key_position: 0,
            },
            ExpectedColumn {
                name: "node_json",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 0,
            },
        ],
        &[
            ExpectedForeignKey {
                sequence: 0,
                referenced_table: "nodes",
                from_column: "story_id",
                to_column: "story_id",
                on_delete: "RESTRICT",
            },
            ExpectedForeignKey {
                sequence: 1,
                referenced_table: "nodes",
                from_column: "parent_id",
                to_column: "node_id",
                on_delete: "RESTRICT",
            },
        ],
    )
}

fn validate_materialized_states_schema(
    connection: &Connection,
    version: i64,
) -> Result<(), StoreError> {
    validate_table(
        connection,
        version,
        "materialized_states",
        &[
            ExpectedColumn {
                name: "story_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 1,
            },
            ExpectedColumn {
                name: "node_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 2,
            },
            ExpectedColumn {
                name: "state_json",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 0,
            },
        ],
        &[
            ExpectedForeignKey {
                sequence: 0,
                referenced_table: "nodes",
                from_column: "story_id",
                to_column: "story_id",
                on_delete: "RESTRICT",
            },
            ExpectedForeignKey {
                sequence: 1,
                referenced_table: "nodes",
                from_column: "node_id",
                to_column: "node_id",
                on_delete: "RESTRICT",
            },
        ],
    )
}

fn validate_branch_heads_schema(connection: &Connection, version: i64) -> Result<(), StoreError> {
    validate_table(
        connection,
        version,
        "branch_heads",
        &[
            ExpectedColumn {
                name: "story_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 1,
            },
            ExpectedColumn {
                name: "branch_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 2,
            },
            ExpectedColumn {
                name: "head_node_id",
                declared_type: "TEXT",
                not_null: true,
                primary_key_position: 0,
            },
        ],
        &[
            ExpectedForeignKey {
                sequence: 0,
                referenced_table: "nodes",
                from_column: "story_id",
                to_column: "story_id",
                on_delete: "RESTRICT",
            },
            ExpectedForeignKey {
                sequence: 1,
                referenced_table: "nodes",
                from_column: "head_node_id",
                to_column: "node_id",
                on_delete: "RESTRICT",
            },
        ],
    )
}

fn validate_table(
    connection: &Connection,
    version: i64,
    table: &str,
    expected_columns: &[ExpectedColumn<'_>],
    expected_foreign_key: &[ExpectedForeignKey<'_>],
) -> Result<(), StoreError> {
    let columns = table_columns(connection, table)?;
    let columns_match = columns.len() == expected_columns.len()
        && columns
            .iter()
            .zip(expected_columns)
            .all(|(actual, expected)| {
                actual.name == expected.name
                    && actual.declared_type == expected.declared_type
                    && actual.not_null == expected.not_null
                    && actual.primary_key_position == expected.primary_key_position
            });
    if !columns_match {
        return Err(incomplete_schema(
            version,
            &format!("table {table} has an unexpected column layout"),
        ));
    }

    let foreign_keys = table_foreign_keys(connection, table)?;
    let foreign_keys_match = foreign_keys.len() == expected_foreign_key.len()
        && foreign_keys
            .iter()
            .zip(expected_foreign_key)
            .all(|(actual, expected)| {
                actual.sequence == expected.sequence
                    && actual.referenced_table == expected.referenced_table
                    && actual.from_column == expected.from_column
                    && actual.to_column == expected.to_column
                    && actual.on_delete == expected.on_delete
            });
    if !foreign_keys_match {
        return Err(incomplete_schema(
            version,
            &format!("table {table} has an unexpected foreign key layout"),
        ));
    }
    Ok(())
}

fn table_columns(connection: &Connection, table: &str) -> Result<Vec<ColumnLayout>, StoreError> {
    let mut statement = connection.prepare(&format!("PRAGMA table_info('{table}')"))?;
    let rows = statement.query_map([], |row| {
        Ok(ColumnLayout {
            name: row.get(1)?,
            declared_type: row.get(2)?,
            not_null: row.get::<_, i64>(3)? != 0,
            primary_key_position: row.get(5)?,
        })
    })?;
    rows.collect::<Result<Vec<_>, _>>()
        .map_err(StoreError::from)
}

fn table_foreign_keys(
    connection: &Connection,
    table: &str,
) -> Result<Vec<ForeignKeyLayout>, StoreError> {
    let mut statement = connection.prepare(&format!("PRAGMA foreign_key_list('{table}')"))?;
    let rows = statement.query_map([], |row| {
        Ok(ForeignKeyLayout {
            sequence: row.get(1)?,
            referenced_table: row.get(2)?,
            from_column: row.get(3)?,
            to_column: row.get(4)?,
            on_delete: row.get(6)?,
        })
    })?;
    rows.collect::<Result<Vec<_>, _>>()
        .map_err(StoreError::from)
}

fn validate_root_index(connection: &Connection, version: i64) -> Result<(), StoreError> {
    let index_layout = connection
        .query_row(
            "SELECT pragma_index_list.[unique], pragma_index_list.partial, sqlite_schema.sql
             FROM pragma_index_list('nodes')
             JOIN sqlite_schema
               ON sqlite_schema.type = 'index'
              AND sqlite_schema.name = pragma_index_list.name
             WHERE pragma_index_list.name = 'one_root_per_story'",
            [],
            |row| {
                Ok((
                    row.get::<_, bool>(0)?,
                    row.get::<_, bool>(1)?,
                    row.get::<_, String>(2)?,
                ))
            },
        )
        .optional()?;
    let Some((is_unique, is_partial, index_sql)) = index_layout else {
        return Err(incomplete_schema(
            version,
            "unique partial index one_root_per_story is missing or incompatible",
        ));
    };
    let normalized_sql = index_sql
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .to_ascii_lowercase();
    if !is_unique
        || !is_partial
        || normalized_sql
            != "create unique index one_root_per_story on nodes (story_id) where parent_id is null"
    {
        return Err(incomplete_schema(
            version,
            "unique partial index one_root_per_story is missing or incompatible",
        ));
    }

    let mut statement = connection
        .prepare("SELECT name FROM pragma_index_info('one_root_per_story') ORDER BY seqno")?;
    let columns = statement
        .query_map([], |row| row.get::<_, String>(0))?
        .collect::<Result<Vec<_>, _>>()?;
    if columns != vec!["story_id".to_owned()] {
        return Err(incomplete_schema(
            version,
            "index one_root_per_story targets unexpected columns",
        ));
    }
    Ok(())
}

fn incomplete_schema(version: i64, detail: &str) -> StoreError {
    StoreError::Sqlite(format!(
        "story database schema version {version} is incomplete or incompatible: {detail}"
    ))
}

fn unsupported_schema_version(found: i64) -> StoreError {
    StoreError::Sqlite(format!(
        "unsupported story database schema version {found}; newest supported version is \
         {SCHEMA_VERSION}"
    ))
}

fn connection_configuration(detail: &str) -> StoreError {
    StoreError::Sqlite(format!(
        "SQLite connection is not safely configured: {detail}"
    ))
}

fn validate_new_branch_id(branch_id: &str) -> Result<(), ForkError> {
    const MAX_BRANCH_ID_BYTES: usize = 128;

    let is_valid = !branch_id.is_empty()
        && branch_id.len() <= MAX_BRANCH_ID_BYTES
        && branch_id
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'));
    if !is_valid {
        return Err(ForkError::InvalidBranchId(branch_id.to_owned()));
    }
    Ok(())
}

fn validate_materialized_state(node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError> {
    if node.story_id != state.story_id {
        return Err(StoreError::StateMismatch("story_id"));
    }
    if node.id != state.current_node {
        return Err(StoreError::StateMismatch("current_node"));
    }
    if node.branch_id != state.current_branch {
        return Err(StoreError::StateMismatch("current_branch"));
    }
    validate_state_hash(node, state)?;
    Ok(())
}

fn restore_state_for_branch(
    node: &StoryNode,
    state: &RuntimeState,
    story_id: &str,
    node_id: &str,
    branch_id: &str,
) -> Result<RuntimeState, StoreError> {
    // A branch created from history points at the original immutable node.
    // Validate the persisted pair with its original branch identity before
    // normalizing only the caller-visible cursor.
    validate_loaded_node(
        node,
        story_id,
        node_id,
        &node.branch_id,
        node.parent_id.as_deref(),
    )?;
    validate_loaded_state(state, story_id, node_id, &node.branch_id)?;
    validate_state_hash(node, state)?;

    let mut restored = state.clone();
    branch_id.clone_into(&mut restored.current_branch);
    Ok(restored)
}

fn deserialize_node(node_json: &str) -> Result<StoryNode, StoreError> {
    serde_json::from_str(node_json).map_err(StoreError::from)
}

fn deserialize_state(state_json: &str) -> Result<RuntimeState, StoreError> {
    serde_json::from_str(state_json).map_err(StoreError::from)
}

fn validate_loaded_node(
    node: &StoryNode,
    story_id: &str,
    node_id: &str,
    branch_id: &str,
    parent_id: Option<&str>,
) -> Result<(), StoreError> {
    if node.story_id != story_id {
        return Err(StoreError::StateMismatch("story_id"));
    }
    if node.id != node_id {
        return Err(StoreError::StateMismatch("current_node"));
    }
    if node.branch_id != branch_id {
        return Err(StoreError::StateMismatch("current_branch"));
    }
    if node.parent_id.as_deref() != parent_id {
        return Err(StoreError::StateMismatch("parent_id"));
    }
    Ok(())
}

fn validate_loaded_state(
    state: &RuntimeState,
    story_id: &str,
    node_id: &str,
    branch_id: &str,
) -> Result<(), StoreError> {
    if state.story_id != story_id {
        return Err(StoreError::StateMismatch("story_id"));
    }
    if state.current_node != node_id {
        return Err(StoreError::StateMismatch("current_node"));
    }
    if state.current_branch != branch_id {
        return Err(StoreError::StateMismatch("current_branch"));
    }
    Ok(())
}

fn validate_state_hash(node: &StoryNode, state: &RuntimeState) -> Result<(), StoreError> {
    let bytes = serde_json::to_vec(state)?;
    if stable_json_hash(&bytes) != node.state_hash {
        return Err(StoreError::StateHashMismatch {
            node_id: node.id.clone(),
        });
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::{
        collections::BTreeMap,
        fs,
        path::{Path, PathBuf},
        time::{SystemTime, UNIX_EPOCH},
    };

    use nana_domain::{
        PresentationSnapshot, RuntimeState, StateDelta, StoryNode, stable_json_hash,
    };
    use rusqlite::{Connection, params};

    use super::{
        BUSY_TIMEOUT_MILLIS, ForkError, InMemoryStoryStore, SCHEMA_VERSION, SqliteStoryStore,
        StoreError, StoryStore,
    };

    fn state(node: &str, branch: &str) -> RuntimeState {
        RuntimeState {
            story_id: "story_demo".to_owned(),
            current_node: node.to_owned(),
            current_branch: branch.to_owned(),
            world_flags: BTreeMap::new(),
            relationships: BTreeMap::new(),
            relationship_states: Vec::new(),
            promises: Vec::new(),
            knowledge: Vec::new(),
            items: Vec::new(),
            clocks: Vec::new(),
            checks: Vec::new(),
        }
    }

    fn node(id: &str, parent_id: Option<&str>, branch: &str) -> StoryNode {
        let materialized = state(id, branch);
        StoryNode {
            id: id.to_owned(),
            story_id: "story_demo".to_owned(),
            branch_id: branch.to_owned(),
            parent_id: parent_id.map(ToOwned::to_owned),
            action_id: format!("action_{id}"),
            user_input: String::new(),
            presentation: PresentationSnapshot::default(),
            delta: StateDelta { ops: Vec::new() },
            state_hash: stable_json_hash(
                &serde_json::to_vec(&materialized).expect("serializable test state"),
            ),
        }
    }

    trait InspectableStoryStore: StoryStore {
        fn inspected_branch_head(
            &self,
            story_id: &str,
            branch_id: &str,
        ) -> Result<Option<String>, StoreError>;

        fn inspected_state_at_node(
            &self,
            story_id: &str,
            node_id: &str,
        ) -> Result<RuntimeState, StoreError>;
    }

    impl InspectableStoryStore for InMemoryStoryStore {
        fn inspected_branch_head(
            &self,
            story_id: &str,
            branch_id: &str,
        ) -> Result<Option<String>, StoreError> {
            self.branch_head(story_id, branch_id)
        }

        fn inspected_state_at_node(
            &self,
            story_id: &str,
            node_id: &str,
        ) -> Result<RuntimeState, StoreError> {
            self.load_state_at_node(story_id, node_id)
        }
    }

    impl InspectableStoryStore for SqliteStoryStore {
        fn inspected_branch_head(
            &self,
            story_id: &str,
            branch_id: &str,
        ) -> Result<Option<String>, StoreError> {
            self.branch_head(story_id, branch_id)
        }

        fn inspected_state_at_node(
            &self,
            story_id: &str,
            node_id: &str,
        ) -> Result<RuntimeState, StoreError> {
            self.load_state_at_node(story_id, node_id)
        }
    }

    fn assert_appends_and_loads_the_branch_head(store: &impl InspectableStoryStore) {
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .append_node(
                &node("node_002", Some("node_001"), "branch_main"),
                &state("node_002", "branch_main"),
            )
            .expect("child append");

        assert_eq!(
            store
                .load_state("story_demo", "branch_main")
                .expect("stored state")
                .current_node,
            "node_002"
        );
        assert_eq!(
            store
                .inspected_state_at_node("story_demo", "node_001")
                .expect("root state")
                .current_node,
            "node_001"
        );
        assert_eq!(
            store
                .load_node("story_demo", "node_002")
                .expect("stored node")
                .parent_id
                .as_deref(),
            Some("node_001")
        );
    }

    fn assert_forks_from_an_old_node(store: &impl InspectableStoryStore) {
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .append_node(
                &node("node_002", Some("node_001"), "branch_main"),
                &state("node_002", "branch_main"),
            )
            .expect("main append");
        store
            .append_node(
                &node("node_003", Some("node_001"), "branch_rewind"),
                &state("node_003", "branch_rewind"),
            )
            .expect("fork append");

        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_main")
                .expect("main head"),
            Some("node_002".to_owned())
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_rewind")
                .expect("fork head"),
            Some("node_003".to_owned())
        );
    }

    fn assert_rejects_a_stale_append(store: &impl InspectableStoryStore) {
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .append_node(
                &node("node_002", Some("node_001"), "branch_main"),
                &state("node_002", "branch_main"),
            )
            .expect("main append");

        assert_eq!(
            store.append_node(
                &node("node_stale", Some("node_001"), "branch_main"),
                &state("node_stale", "branch_main"),
            ),
            Err(StoreError::StaleBranchHead {
                expected: "node_002".to_owned(),
                actual: "node_001".to_owned(),
            })
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_main")
                .expect("main head"),
            Some("node_002".to_owned())
        );
        assert_eq!(
            store.inspected_state_at_node("story_demo", "node_stale"),
            Err(StoreError::ParentNotFound("node_stale".to_owned()))
        );
    }

    fn assert_rejects_state_mismatches(store: &impl InspectableStoryStore) {
        let root = node("node_001", None, "branch_main");

        let mut wrong_story = state("node_001", "branch_main");
        wrong_story.story_id = "another_story".to_owned();
        assert_eq!(
            store.append_node(&root, &wrong_story),
            Err(StoreError::StateMismatch("story_id"))
        );

        assert_eq!(
            store.append_node(&root, &state("different_node", "branch_main")),
            Err(StoreError::StateMismatch("current_node"))
        );
        assert_eq!(
            store.append_node(&root, &state("node_001", "different_branch")),
            Err(StoreError::StateMismatch("current_branch"))
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_main")
                .expect("empty store"),
            None
        );
    }

    fn seed_historical_main(store: &impl InspectableStoryStore) {
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .append_node(
                &node("node_002", Some("node_001"), "branch_main"),
                &state("node_002", "branch_main"),
            )
            .expect("main append");
    }

    fn assert_creates_independent_branches_from_history(store: &impl InspectableStoryStore) {
        seed_historical_main(store);

        let alpha = store
            .fork_branch("story_demo", "node_001", "branch_alpha")
            .expect("first historical fork");
        let beta = store
            .fork_branch("story_demo", "node_001", "branch_beta")
            .expect("second historical fork");

        assert_eq!(alpha.current_node, "node_001");
        assert_eq!(alpha.current_branch, "branch_alpha");
        assert_eq!(beta.current_node, "node_001");
        assert_eq!(beta.current_branch, "branch_beta");
        assert_eq!(
            store
                .load_node("story_demo", "node_001")
                .expect("immutable source node")
                .branch_id,
            "branch_main"
        );
        assert_eq!(
            store
                .inspected_state_at_node("story_demo", "node_001")
                .expect("immutable source state")
                .current_branch,
            "branch_main"
        );

        store
            .append_node(
                &node("node_alpha", Some("node_001"), "branch_alpha"),
                &state("node_alpha", "branch_alpha"),
            )
            .expect("append on first fork");

        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_main")
                .expect("main head"),
            Some("node_002".to_owned())
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_alpha")
                .expect("alpha head"),
            Some("node_alpha".to_owned())
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_beta")
                .expect("beta head"),
            Some("node_001".to_owned())
        );
        assert_eq!(
            store
                .load_state("story_demo", "branch_beta")
                .expect("normalized beta state")
                .current_branch,
            "branch_beta"
        );

        assert_eq!(
            store.append_node(
                &node("node_stale", Some("node_001"), "branch_alpha"),
                &state("node_stale", "branch_alpha"),
            ),
            Err(StoreError::StaleBranchHead {
                expected: "node_alpha".to_owned(),
                actual: "node_001".to_owned(),
            })
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_alpha")
                .expect("unchanged alpha head"),
            Some("node_alpha".to_owned())
        );
    }

    fn assert_rejects_invalid_duplicate_and_unknown_forks(store: &impl InspectableStoryStore) {
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");

        for invalid in ["", " ", "branch/slash", "分支"] {
            assert_eq!(
                store.fork_branch("story_demo", "node_001", invalid),
                Err(ForkError::InvalidBranchId(invalid.to_owned()))
            );
            assert_eq!(
                store
                    .inspected_branch_head("story_demo", invalid)
                    .expect("invalid branch lookup"),
                None
            );
        }

        assert_eq!(
            store.fork_branch("story_demo", "unknown_node", "branch_unknown"),
            Err(ForkError::Store(StoreError::ParentNotFound(
                "unknown_node".to_owned()
            )))
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_unknown")
                .expect("unknown source fork"),
            None
        );

        store
            .fork_branch("story_demo", "node_001", "branch_new")
            .expect("initial fork");
        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_new"),
            Err(ForkError::BranchAlreadyExists {
                story_id: "story_demo".to_owned(),
                branch_id: "branch_new".to_owned(),
            })
        );
        assert_eq!(
            store
                .inspected_branch_head("story_demo", "branch_new")
                .expect("duplicate fork"),
            Some("node_001".to_owned())
        );
    }

    #[test]
    fn memory_appends_and_loads_the_branch_head() {
        let store = InMemoryStoryStore::new();
        assert_appends_and_loads_the_branch_head(&store);
    }

    #[test]
    fn sqlite_appends_and_loads_the_branch_head() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_appends_and_loads_the_branch_head(&store);
    }

    #[test]
    fn memory_forks_from_an_old_node_without_moving_the_source_head() {
        let store = InMemoryStoryStore::new();
        assert_forks_from_an_old_node(&store);
    }

    #[test]
    fn sqlite_forks_from_an_old_node_without_moving_the_source_head() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_forks_from_an_old_node(&store);
    }

    #[test]
    fn memory_creates_independent_branch_heads_from_historical_nodes() {
        let store = InMemoryStoryStore::new();
        assert_creates_independent_branches_from_history(&store);
    }

    #[test]
    fn sqlite_creates_independent_branch_heads_from_historical_nodes() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_creates_independent_branches_from_history(&store);
    }

    #[test]
    fn memory_rejects_invalid_duplicate_and_unknown_forks_atomically() {
        let store = InMemoryStoryStore::new();
        assert_rejects_invalid_duplicate_and_unknown_forks(&store);
    }

    #[test]
    fn sqlite_rejects_invalid_duplicate_and_unknown_forks_atomically() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_rejects_invalid_duplicate_and_unknown_forks(&store);
    }

    #[test]
    fn memory_rejects_a_stale_append_without_moving_the_head() {
        let store = InMemoryStoryStore::new();
        assert_rejects_a_stale_append(&store);
    }

    #[test]
    fn sqlite_rejects_a_stale_append_without_moving_the_head() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_rejects_a_stale_append(&store);
    }

    #[test]
    fn memory_rejects_state_that_does_not_match_the_node() {
        let store = InMemoryStoryStore::new();
        assert_rejects_state_mismatches(&store);
    }

    #[test]
    fn sqlite_rejects_state_that_does_not_match_the_node() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        assert_rejects_state_mismatches(&store);
    }

    #[test]
    fn sqlite_enforces_node_and_parent_rules_atomically() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        let root = node("node_001", None, "branch_main");
        let root_state = state("node_001", "branch_main");
        store.append_node(&root, &root_state).expect("root append");

        assert_eq!(
            store.append_node(&root, &root_state),
            Err(StoreError::NodeAlreadyExists("node_001".to_owned()))
        );
        assert_eq!(
            store.append_node(
                &node("node_missing_parent", Some("unknown"), "branch_fork"),
                &state("node_missing_parent", "branch_fork"),
            ),
            Err(StoreError::ParentNotFound("unknown".to_owned()))
        );
        assert_eq!(
            store.append_node(
                &node("node_second_root", None, "branch_other"),
                &state("node_second_root", "branch_other"),
            ),
            Err(StoreError::ParentNotFound(
                "a non-root node must name its parent".to_owned()
            ))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_main")
                .expect("main head"),
            Some("node_001".to_owned())
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_fork")
                .expect("failed fork"),
            None
        );
    }

    #[test]
    fn sqlite_reports_corrupt_json_without_panicking() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .connection
            .lock()
            .expect("SQLite connection lock")
            .execute(
                "UPDATE materialized_states SET state_json = ?1",
                params!["{"],
            )
            .expect("corrupt test state");

        assert!(matches!(
            store.load_state("story_demo", "branch_main"),
            Err(StoreError::Serialization(_))
        ));
    }

    #[test]
    fn sqlite_rejects_valid_state_json_with_a_mismatched_hash() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        let mut corrupted = state("node_001", "branch_main");
        corrupted
            .world_flags
            .insert("silently_changed".to_owned(), true);
        let corrupted_json = serde_json::to_string(&corrupted).expect("corrupt test JSON");
        store
            .connection
            .lock()
            .expect("SQLite connection lock")
            .execute(
                "UPDATE materialized_states SET state_json = ?1",
                params![corrupted_json],
            )
            .expect("corrupt test state");

        assert_eq!(
            store.load_state("story_demo", "branch_main"),
            Err(StoreError::StateHashMismatch {
                node_id: "node_001".to_owned()
            })
        );
        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_corrupt"),
            Err(ForkError::Store(StoreError::StateHashMismatch {
                node_id: "node_001".to_owned()
            }))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_corrupt")
                .expect("failed corrupt fork"),
            None
        );
    }

    #[test]
    fn memory_rejects_a_corrupt_source_hash_without_creating_a_branch() {
        let store = InMemoryStoryStore::new();
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .data
            .lock()
            .expect("memory store lock")
            .states
            .get_mut(&("story_demo".to_owned(), "node_001".to_owned()))
            .expect("source state")
            .world_flags
            .insert("silently_changed".to_owned(), true);

        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_corrupt"),
            Err(ForkError::Store(StoreError::StateHashMismatch {
                node_id: "node_001".to_owned()
            }))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_corrupt")
                .expect("failed corrupt fork"),
            None
        );
    }

    #[test]
    fn memory_rejects_a_source_without_materialized_state() {
        let store = InMemoryStoryStore::new();
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .data
            .lock()
            .expect("memory store lock")
            .states
            .remove(&("story_demo".to_owned(), "node_001".to_owned()));

        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_missing_state"),
            Err(ForkError::Store(StoreError::StateMismatch(
                "node has no materialized state"
            )))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_missing_state")
                .expect("failed missing-state fork"),
            None
        );
    }

    #[test]
    fn sqlite_rejects_a_source_without_materialized_state() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .connection
            .lock()
            .expect("SQLite connection lock")
            .execute(
                "DELETE FROM materialized_states
                 WHERE story_id = ?1 AND node_id = ?2",
                params!["story_demo", "node_001"],
            )
            .expect("remove source state");

        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_missing_state"),
            Err(ForkError::Store(StoreError::StateMismatch(
                "node has no materialized state"
            )))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_missing_state")
                .expect("failed missing-state fork"),
            None
        );
    }

    #[test]
    fn sqlite_rejects_node_json_that_disagrees_with_structure_columns() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        let mut corrupted = node("node_001", None, "branch_main");
        corrupted.branch_id = "hidden_branch".to_owned();
        let corrupted_json = serde_json::to_string(&corrupted).expect("corrupt node JSON");
        store
            .connection
            .lock()
            .expect("SQLite connection lock")
            .execute("UPDATE nodes SET node_json = ?1", params![corrupted_json])
            .expect("corrupt test node");

        assert_eq!(
            store.load_node("story_demo", "node_001"),
            Err(StoreError::StateMismatch("current_branch"))
        );
        assert_eq!(
            store.fork_branch("story_demo", "node_001", "branch_corrupt"),
            Err(ForkError::Store(StoreError::StateMismatch(
                "current_branch"
            )))
        );
        assert_eq!(
            store
                .branch_head("story_demo", "branch_corrupt")
                .expect("failed corrupt fork"),
            None
        );
    }

    #[test]
    fn sqlite_normalizes_a_cross_branch_head_after_verifying_the_original_state() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        store
            .append_node(
                &node("node_001", None, "branch_main"),
                &state("node_001", "branch_main"),
            )
            .expect("root append");
        store
            .append_node(
                &node("node_other", Some("node_001"), "branch_other"),
                &state("node_other", "branch_other"),
            )
            .expect("other branch append");
        store
            .connection
            .lock()
            .expect("SQLite connection lock")
            .execute(
                "UPDATE branch_heads
                 SET head_node_id = 'node_other'
                 WHERE story_id = 'story_demo' AND branch_id = 'branch_main'",
                [],
            )
            .expect("corrupt branch head");

        let restored = store
            .load_state("story_demo", "branch_main")
            .expect("cross-branch head");
        assert_eq!(restored.current_node, "node_other");
        assert_eq!(restored.current_branch, "branch_main");
        assert_eq!(
            store
                .load_node("story_demo", "node_other")
                .expect("immutable node")
                .branch_id,
            "branch_other"
        );
    }

    struct TemporaryDatabase {
        path: PathBuf,
    }

    impl TemporaryDatabase {
        fn new() -> Self {
            let nonce = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos();
            Self {
                path: std::env::temp_dir()
                    .join(format!("nana-store-{}-{nonce}.sqlite3", std::process::id())),
            }
        }

        fn path(&self) -> &Path {
            &self.path
        }
    }

    impl Drop for TemporaryDatabase {
        fn drop(&mut self) {
            for suffix in ["", "-wal", "-shm"] {
                let mut path = self.path.as_os_str().to_os_string();
                path.push(suffix);
                let path = PathBuf::from(path);
                if path.exists() {
                    fs::remove_file(path).expect("remove temporary SQLite database file");
                }
            }
        }
    }

    #[test]
    fn sqlite_initializes_a_new_database_with_version_and_safe_pragmas() {
        let database = TemporaryDatabase::new();
        let store = SqliteStoryStore::open(database.path()).expect("new file SQLite store");
        let connection = store.connection.lock().expect("SQLite connection lock");

        assert_eq!(
            connection
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("schema version"),
            SCHEMA_VERSION
        );
        assert!(
            connection
                .query_row("PRAGMA foreign_keys", [], |row| row.get::<_, bool>(0))
                .expect("foreign key pragma")
        );
        assert_eq!(
            connection
                .query_row("PRAGMA busy_timeout", [], |row| row.get::<_, i64>(0))
                .expect("busy timeout pragma"),
            BUSY_TIMEOUT_MILLIS
        );
        assert_eq!(
            connection
                .query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))
                .expect("journal mode")
                .to_ascii_lowercase(),
            "wal"
        );
        assert_eq!(
            connection
                .query_row("PRAGMA synchronous", [], |row| row.get::<_, i64>(0))
                .expect("synchronous pragma"),
            1
        );
    }

    #[test]
    fn sqlite_in_memory_uses_safe_pragmas_without_requesting_wal() {
        let store = SqliteStoryStore::open_in_memory().expect("in-memory SQLite store");
        let connection = store.connection.lock().expect("SQLite connection lock");

        assert_eq!(
            connection
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("schema version"),
            SCHEMA_VERSION
        );
        assert!(
            connection
                .query_row("PRAGMA foreign_keys", [], |row| row.get::<_, bool>(0))
                .expect("foreign key pragma")
        );
        assert_eq!(
            connection
                .query_row("PRAGMA busy_timeout", [], |row| row.get::<_, i64>(0))
                .expect("busy timeout pragma"),
            BUSY_TIMEOUT_MILLIS
        );
        assert_eq!(
            connection
                .query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))
                .expect("journal mode")
                .to_ascii_lowercase(),
            "memory"
        );
    }

    #[test]
    fn sqlite_adopts_a_complete_unversioned_database_without_losing_data() {
        let database = TemporaryDatabase::new();
        {
            let store = SqliteStoryStore::open(database.path()).expect("file SQLite store");
            store
                .append_node(
                    &node("node_001", None, "branch_main"),
                    &state("node_001", "branch_main"),
                )
                .expect("root append");
            store
                .connection
                .lock()
                .expect("SQLite connection lock")
                .execute_batch("PRAGMA user_version = 0;")
                .expect("simulate Wave 2 database");
        }

        let reopened =
            SqliteStoryStore::open(database.path()).expect("adopt complete legacy database");
        assert_eq!(
            reopened
                .load_state("story_demo", "branch_main")
                .expect("legacy state")
                .current_node,
            "node_001"
        );
        assert_eq!(
            reopened
                .connection
                .lock()
                .expect("SQLite connection lock")
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("adopted schema version"),
            SCHEMA_VERSION
        );
    }

    #[test]
    fn sqlite_rejects_an_unknown_future_schema_version_without_changing_it() {
        let database = TemporaryDatabase::new();
        {
            let connection = Connection::open(database.path()).expect("raw SQLite database");
            connection
                .execute_batch("PRAGMA user_version = 99;")
                .expect("future schema version");
        }

        assert!(matches!(
            SqliteStoryStore::open(database.path()),
            Err(StoreError::Sqlite(message))
                if message.contains("unsupported story database schema version 99")
        ));

        let connection = Connection::open(database.path()).expect("reopen raw SQLite database");
        assert_eq!(
            connection
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("future schema version"),
            99
        );
    }

    #[test]
    fn sqlite_rejects_an_incomplete_unversioned_schema_without_rebuilding_it() {
        let database = TemporaryDatabase::new();
        {
            let connection = Connection::open(database.path()).expect("raw SQLite database");
            connection
                .execute_batch(
                    "CREATE TABLE nodes (
                         story_id TEXT NOT NULL,
                         node_id TEXT NOT NULL,
                         branch_id TEXT NOT NULL,
                         parent_id TEXT,
                         node_json TEXT NOT NULL,
                         PRIMARY KEY (story_id, node_id)
                     );",
                )
                .expect("partial legacy schema");
        }

        assert!(matches!(
            SqliteStoryStore::open(database.path()),
            Err(StoreError::Sqlite(message))
                if message.contains("schema version 0 is incomplete or incompatible")
        ));

        let connection = Connection::open(database.path()).expect("reopen raw SQLite database");
        assert_eq!(
            connection
                .query_row(
                    "SELECT COUNT(*) FROM sqlite_schema
                     WHERE type = 'table'
                       AND name IN ('nodes', 'materialized_states', 'branch_heads')",
                    [],
                    |row| row.get::<_, i64>(0),
                )
                .expect("application table count"),
            1
        );
        assert_eq!(
            connection
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("unchanged schema version"),
            0
        );
    }

    #[test]
    fn sqlite_rejects_a_damaged_current_schema_without_rebuilding_it() {
        let database = TemporaryDatabase::new();
        {
            let store = SqliteStoryStore::open(database.path()).expect("file SQLite store");
            store
                .connection
                .lock()
                .expect("SQLite connection lock")
                .execute_batch("DROP TABLE branch_heads;")
                .expect("damage schema");
        }

        assert!(matches!(
            SqliteStoryStore::open(database.path()),
            Err(StoreError::Sqlite(message))
                if message.contains("schema version 1 is incomplete or incompatible")
        ));

        let connection = Connection::open(database.path()).expect("reopen raw SQLite database");
        assert!(
            !connection
                .query_row(
                    "SELECT EXISTS(
                         SELECT 1 FROM sqlite_schema
                         WHERE type = 'table' AND name = 'branch_heads'
                     )",
                    [],
                    |row| row.get::<_, bool>(0),
                )
                .expect("branch_heads presence")
        );
    }

    #[test]
    fn sqlite_reopens_and_preserves_nodes_states_and_heads() {
        let database = TemporaryDatabase::new();
        {
            let store = SqliteStoryStore::open(database.path()).expect("file SQLite store");
            store
                .append_node(
                    &node("node_001", None, "branch_main"),
                    &state("node_001", "branch_main"),
                )
                .expect("root append");
            store
                .append_node(
                    &node("node_002", Some("node_001"), "branch_main"),
                    &state("node_002", "branch_main"),
                )
                .expect("child append");
        }

        let reopened = SqliteStoryStore::open(database.path()).expect("reopened SQLite store");
        assert_eq!(
            reopened
                .connection
                .lock()
                .expect("SQLite connection lock")
                .query_row("PRAGMA user_version", [], |row| row.get::<_, i64>(0))
                .expect("reopened schema version"),
            SCHEMA_VERSION
        );
        assert_eq!(
            reopened
                .branch_head("story_demo", "branch_main")
                .expect("persisted head"),
            Some("node_002".to_owned())
        );
        assert_eq!(
            reopened
                .load_state("story_demo", "branch_main")
                .expect("persisted head state")
                .current_node,
            "node_002"
        );
        assert_eq!(
            reopened
                .load_state_at_node("story_demo", "node_001")
                .expect("persisted root state")
                .current_node,
            "node_001"
        );
    }

    #[test]
    fn sqlite_reopens_a_historical_fork_and_can_append_from_its_source() {
        let database = TemporaryDatabase::new();
        {
            let store = SqliteStoryStore::open(database.path()).expect("file SQLite store");
            seed_historical_main(&store);
            let forked = store
                .fork_branch("story_demo", "node_001", "branch_reopened")
                .expect("historical fork");
            assert_eq!(forked.current_node, "node_001");
            assert_eq!(forked.current_branch, "branch_reopened");
        }

        let reopened = SqliteStoryStore::open(database.path()).expect("reopened SQLite store");
        let restored = reopened
            .load_state("story_demo", "branch_reopened")
            .expect("persisted fork cursor");
        assert_eq!(restored.current_node, "node_001");
        assert_eq!(restored.current_branch, "branch_reopened");
        assert_eq!(
            reopened
                .branch_head("story_demo", "branch_main")
                .expect("unchanged main head"),
            Some("node_002".to_owned())
        );

        reopened
            .append_node(
                &node("node_reopened", Some("node_001"), "branch_reopened"),
                &state("node_reopened", "branch_reopened"),
            )
            .expect("append after reopen");
        assert_eq!(
            reopened
                .branch_head("story_demo", "branch_reopened")
                .expect("advanced reopened branch"),
            Some("node_reopened".to_owned())
        );
        assert_eq!(
            reopened
                .branch_head("story_demo", "branch_main")
                .expect("main remains unchanged"),
            Some("node_002".to_owned())
        );
    }
}
