use std::collections::{BTreeSet, VecDeque};

use nana_domain::{
    PlayerView, PresentationSnapshot, RuntimeState, StateDelta, StoryNode, TurnFailure,
    TurnFailureCode, TurnIntent, TurnRequest, TurnResult, WorldBookEntry,
};
use nana_engine::{ReduceError, apply_delta};
use nana_store::{StoreError, StoryStore};
use thiserror::Error;

mod adjudication;
mod context;
mod lapp_provider;

pub use adjudication::{
    AdjudicatingTurnPlanProvider, AdjudicationCatalog, AdjudicationError, AdjudicationModel,
    AdjudicationModelInput, AdjudicationModelResponse, AdjudicationRunError, AdjudicationToolCall,
    CatalogError, DEFAULT_MAX_ADJUDICATION_STEPS, HIDDEN_CHECK_TOOL_NAME, HiddenCheckRequest,
    QualitativeCheckOutcome, classify_roll, deterministic_roll,
};
pub use context::{
    CharacterMemory, CompiledSceneContext, ContextBudget, ContextCharacterCard,
    ContextCompileError, ContextInventoryItem, ContextJudgmentRule, ContextPersona,
    ContextPlotEvent, ContextPlotOutcome, ContextPlotPressure, ContextSkill, ContextStateMemory,
    ContextStatePosition, ContextSummary, ContextTurn, ContextWorldBookEntry, HiddenCheckTreatment,
    NarrativeSafety, PlayerMemory, ResourceProvenance, ResourceStringTreatment,
    SCENE_CONTEXT_SCHEMA_VERSION, SceneContext, SharedMemory, SummaryClassification, SummaryMemory,
    SummaryTreatment, compile_scene_context, compile_scene_context_with_budget,
    encode_compiled_scene_context,
};
pub use lapp_provider::{
    ChatExecutor, LappAdjudicationModel, LappTurnPlanProvider, OpenLappChatExecutor,
    TURN_PLAN_TOOL_NAME,
};

pub const LAPP_BASELINE_COMMIT: &str = "5ba3c659e1536ec4bee16340faca603940a5cb17";
pub const MAX_WORLD_BOOK_ENTRIES: usize = 8;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidModelOutputKind {
    InvalidJson,
    InvalidSchema,
    InvalidShape,
    InvalidPlan,
}

#[derive(Debug, Error)]
pub enum ProviderError {
    #[error("no recorded response remains")]
    FixtureExhausted,
    #[error("LAPP profile could not be loaded")]
    Profile { code: openlapp::ErrorCode },
    #[error("LAPP chat client could not be configured")]
    Configuration { code: Option<openlapp::ErrorCode> },
    #[error("LAPP chat request failed")]
    Upstream { code: Option<openlapp::ErrorCode> },
    #[error("model returned an invalid turn plan")]
    InvalidModelOutput { kind: InvalidModelOutputKind },
    #[error("turn context could not be encoded")]
    ContextEncoding,
}

pub trait TurnProvider {
    fn complete_turn(&mut self, request: &TurnRequest) -> Result<TurnResult, ProviderError>;
}

/// Non-view model output used by the persistent turn engine.
///
/// The provider can propose player-facing presentation and state changes, but it
/// cannot construct the final [`PlayerView`]. That view is derived from committed
/// state by a separate trusted projection boundary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnPlan {
    pub committed_node_id: String,
    pub presentation: PresentationSnapshot,
    pub delta: StateDelta,
}

/// Produces the uncommitted model plan for a turn.
pub trait TurnPlanProvider {
    fn plan_turn(
        &mut self,
        request: &TurnRequest,
        state: &RuntimeState,
    ) -> Result<TurnPlan, ProviderError>;
}

impl<Provider: TurnPlanProvider + ?Sized> TurnPlanProvider for &mut Provider {
    fn plan_turn(
        &mut self,
        request: &TurnRequest,
        state: &RuntimeState,
    ) -> Result<TurnPlan, ProviderError> {
        (**self).plan_turn(request, state)
    }
}

/// Projects only already-committed state into the player-safe read model.
///
/// Projection is deliberately infallible: it is a pure, defensive operation
/// that omits or degrades data it cannot safely represent. This prevents a
/// post-commit error window where the branch advances but the turn reports a
/// failure.
pub trait TurnProjector {
    fn project_committed_turn(&mut self, state: &RuntimeState, node: &StoryNode) -> PlayerView;
}

/// Store-backed single-turn coordinator.
///
/// All fallible work is completed before the atomic append. Projection runs
/// only after the store confirms the new branch head.
pub struct TurnEngine<'store, Store, Provider, Projector> {
    store: &'store Store,
    provider: Provider,
    projector: Projector,
}

impl<'store, Store, Provider, Projector> TurnEngine<'store, Store, Provider, Projector>
where
    Store: StoryStore,
    Provider: TurnPlanProvider,
    Projector: TurnProjector,
{
    #[must_use]
    pub fn new(store: &'store Store, provider: Provider, projector: Projector) -> Self {
        Self {
            store,
            provider,
            projector,
        }
    }

    /// Validate, plan, reduce, commit, then project one player turn.
    pub fn submit_turn(&mut self, request: &TurnRequest) -> Result<TurnResult, TurnFailure> {
        validate_turn_request(request)?;

        let current = self
            .store
            .load_state(&request.story_id, &request.branch_id)
            .map_err(|error| map_store_error(&error))?;
        if current.current_node != request.expected_node_id {
            return Err(stale_node());
        }

        let plan = self
            .provider
            .plan_turn(request, &current)
            .map_err(|error| map_provider_error(&error))?;
        validate_turn_plan(request, &plan)?;

        let mut committed = apply_delta(&current, &plan.delta).map_err(map_reduce_error)?;
        committed.current_node.clone_from(&plan.committed_node_id);
        committed.current_branch.clone_from(&request.branch_id);

        let state_hash = hash_runtime_state(&committed)?;
        let node = StoryNode {
            id: plan.committed_node_id.clone(),
            story_id: request.story_id.clone(),
            branch_id: request.branch_id.clone(),
            parent_id: Some(current.current_node),
            action_id: request.action_id.clone(),
            user_input: request.input.clone(),
            presentation: plan.presentation,
            delta: plan.delta,
            state_hash,
        };

        self.store
            .append_node(&node, &committed)
            .map_err(|error| map_store_error(&error))?;

        let mut player_view = self.projector.project_committed_turn(&committed, &node);
        // Identity comes from the committed state, never from projection input.
        // Normalizing these fields keeps even a defensive fallback projector
        // aligned with the commit it represents.
        player_view.story_id.clone_from(&committed.story_id);
        player_view.node_id.clone_from(&committed.current_node);
        player_view.branch_id.clone_from(&committed.current_branch);

        Ok(TurnResult {
            committed_node_id: node.id,
            player_view,
        })
    }

    #[must_use]
    pub fn provider(&self) -> &Provider {
        &self.provider
    }

    #[must_use]
    pub fn projector(&self) -> &Projector {
        &self.projector
    }
}

#[derive(Debug)]
pub struct FakeProvider {
    responses: VecDeque<TurnResult>,
}

impl FakeProvider {
    #[must_use]
    pub fn new(responses: impl IntoIterator<Item = TurnResult>) -> Self {
        Self {
            responses: responses.into_iter().collect(),
        }
    }
}

impl TurnProvider for FakeProvider {
    fn complete_turn(&mut self, _request: &TurnRequest) -> Result<TurnResult, ProviderError> {
        self.responses
            .pop_front()
            .ok_or(ProviderError::FixtureExhausted)
    }
}

/// Validate the parts of a turn request that do not require persisted story state.
///
/// Stale-node detection belongs to the store boundary. This validation deliberately
/// does not infer any semantics for regenerate or pushed-check turns.
pub fn validate_turn_request(request: &TurnRequest) -> Result<(), TurnFailure> {
    validate_required_text("story_id", &request.story_id)?;
    validate_required_text("branch_id", &request.branch_id)?;
    validate_required_text("expected_node_id", &request.expected_node_id)?;
    validate_required_text("action_id", &request.action_id)?;

    if matches!(&request.intent, TurnIntent::SpeakOrAct) && request.input.trim().is_empty() {
        return Err(invalid_input(
            "input must not be empty for a speak_or_act turn",
        ));
    }

    Ok(())
}

/// Execute one provider turn without persistence or network policy.
///
/// This is the deterministic seam used by fixtures and later by the store-backed
/// runtime: request validation happens before provider invocation and the provider
/// result is checked before it can cross into another layer.
pub fn execute_turn(
    provider: &mut impl TurnProvider,
    request: &TurnRequest,
) -> Result<TurnResult, TurnFailure> {
    validate_turn_request(request)?;
    let result = provider
        .complete_turn(request)
        .map_err(|error| map_provider_error(&error))?;
    validate_turn_result(request, &result)?;
    Ok(result)
}

/// Select world-book entries using deterministic lexical triggers.
///
/// Entries retain their input order. Duplicate ids keep the first matching entry.
/// Required flags are an all-of gate; an entry then needs either a matching tag or
/// a non-empty keyword contained in the turn input.
#[must_use]
pub fn select_world_book_entries(
    entries: &[WorldBookEntry],
    active_flags: &BTreeSet<String>,
    trigger_tags: &BTreeSet<String>,
    input: &str,
    limit: usize,
) -> Vec<WorldBookEntry> {
    let effective_limit = limit.min(MAX_WORLD_BOOK_ENTRIES);
    if effective_limit == 0 {
        return Vec::new();
    }

    let normalized_tags = trigger_tags
        .iter()
        .map(|tag| normalize_trigger(tag))
        .filter(|tag| !tag.is_empty())
        .collect::<BTreeSet<_>>();
    let normalized_input = input.to_lowercase();
    let mut selected_ids = BTreeSet::new();
    let mut selected = Vec::new();

    for entry in entries {
        if selected.len() == effective_limit {
            break;
        }
        if !entry
            .required_flags
            .iter()
            .all(|flag| active_flags.contains(flag))
        {
            continue;
        }

        let tag_matches = entry
            .tags
            .iter()
            .map(|tag| normalize_trigger(tag))
            .any(|tag| !tag.is_empty() && normalized_tags.contains(&tag));
        let keyword_matches = entry.keywords.iter().any(|keyword| {
            let keyword = keyword.trim().to_lowercase();
            !keyword.is_empty() && normalized_input.contains(&keyword)
        });

        if (tag_matches || keyword_matches) && selected_ids.insert(entry.id.clone()) {
            selected.push(entry.clone());
        }
    }

    selected
}

pub fn load_default_lapp_profile() -> Result<openlapp::Profile, ProviderError> {
    openlapp::load_default_profile().map_err(|error| ProviderError::Profile { code: error.code() })
}

#[must_use]
pub fn provider_failure(message: impl Into<String>) -> TurnFailure {
    TurnFailure {
        code: TurnFailureCode::ProviderUnavailable,
        message: message.into(),
        retryable: true,
    }
}

fn validate_turn_result(request: &TurnRequest, result: &TurnResult) -> Result<(), TurnFailure> {
    if result.committed_node_id.trim().is_empty() {
        return Err(invalid_model_output("committed node id is empty"));
    }
    if result.committed_node_id == request.expected_node_id {
        return Err(invalid_model_output(
            "provider returned the uncommitted expected node",
        ));
    }
    if result.player_view.story_id != request.story_id {
        return Err(invalid_model_output(
            "player view story does not match the request",
        ));
    }
    if result.player_view.branch_id != request.branch_id {
        return Err(invalid_model_output(
            "player view branch does not match the request",
        ));
    }
    if result.player_view.node_id != result.committed_node_id {
        return Err(invalid_model_output(
            "player view node does not match the committed node",
        ));
    }
    Ok(())
}

fn validate_turn_plan(request: &TurnRequest, plan: &TurnPlan) -> Result<(), TurnFailure> {
    if plan.committed_node_id.trim().is_empty() {
        return Err(invalid_model_output("committed node id is empty"));
    }
    if plan.committed_node_id == request.expected_node_id {
        return Err(invalid_model_output(
            "provider returned the uncommitted expected node",
        ));
    }
    Ok(())
}

fn hash_runtime_state(state: &RuntimeState) -> Result<String, TurnFailure> {
    serde_json::to_vec(state)
        .map(|bytes| nana_domain::stable_json_hash(&bytes))
        .map_err(|_| internal_failure("turn state could not be prepared"))
}

fn map_reduce_error(_error: ReduceError) -> TurnFailure {
    invalid_model_output("turn plan could not be applied")
}

fn map_provider_error(error: &ProviderError) -> TurnFailure {
    match error {
        ProviderError::InvalidModelOutput { .. } => {
            invalid_model_output("model returned an invalid turn plan")
        }
        ProviderError::FixtureExhausted
        | ProviderError::Profile { .. }
        | ProviderError::Configuration { .. }
        | ProviderError::Upstream { .. }
        | ProviderError::ContextEncoding => provider_unavailable(),
    }
}

fn map_store_error(error: &StoreError) -> TurnFailure {
    match error {
        StoreError::StaleBranchHead { .. } => stale_node(),
        StoreError::StoryNotFound(_) | StoreError::BranchNotFound { .. } => TurnFailure {
            code: TurnFailureCode::InvalidInput,
            message: "story or branch is unavailable".into(),
            retryable: false,
        },
        StoreError::NodeAlreadyExists(_)
        | StoreError::ParentNotFound(_)
        | StoreError::StateMismatch(_)
        | StoreError::StateHashMismatch { .. }
        | StoreError::Sqlite(_)
        | StoreError::Serialization(_)
        | StoreError::Poisoned => internal_failure("turn could not be committed"),
    }
}

fn validate_required_text(field: &str, value: &str) -> Result<(), TurnFailure> {
    if value.trim().is_empty() {
        Err(invalid_input(format!("{field} must not be empty")))
    } else {
        Ok(())
    }
}

fn invalid_input(message: impl Into<String>) -> TurnFailure {
    TurnFailure {
        code: TurnFailureCode::InvalidInput,
        message: message.into(),
        retryable: false,
    }
}

fn invalid_model_output(message: impl Into<String>) -> TurnFailure {
    TurnFailure {
        code: TurnFailureCode::InvalidModelOutput,
        message: message.into(),
        retryable: true,
    }
}

fn provider_unavailable() -> TurnFailure {
    provider_failure("turn provider is unavailable")
}

fn stale_node() -> TurnFailure {
    TurnFailure {
        code: TurnFailureCode::StaleNode,
        message: "story branch changed; refresh and retry".into(),
        retryable: true,
    }
}

fn internal_failure(message: impl Into<String>) -> TurnFailure {
    TurnFailure {
        code: TurnFailureCode::Internal,
        message: message.into(),
        retryable: true,
    }
}

fn normalize_trigger(value: &str) -> String {
    value.trim().to_lowercase()
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use nana_domain::{
        PlayerView, RelationshipBand, RelationshipView, TurnFailureCode, TurnIntent, TurnRequest,
        TurnResult, WorldBookEntry,
    };

    use super::{
        FakeProvider, MAX_WORLD_BOOK_ENTRIES, ProviderError, TurnProvider, execute_turn,
        select_world_book_entries, validate_turn_request,
    };

    fn request(intent: TurnIntent, input: &str) -> TurnRequest {
        TurnRequest {
            story_id: "story_1".into(),
            branch_id: "branch_main".into(),
            expected_node_id: "node_1".into(),
            action_id: "action_1".into(),
            intent,
            input: input.into(),
        }
    }

    fn player_view(node_id: &str) -> PlayerView {
        PlayerView {
            story_id: "story_1".into(),
            node_id: node_id.into(),
            branch_id: "branch_main".into(),
            scene_id: "station".into(),
            scene_title: "Station".into(),
            character_name: "Nana".into(),
            character_expression: None,
            character_pose: None,
            beats: Vec::new(),
            suggestions: Vec::new(),
            inventory: Vec::new(),
            knowledge: Vec::new(),
            promises: Vec::new(),
            relationship: RelationshipView {
                affinity: RelationshipBand::Warming,
                trust: RelationshipBand::Guarded,
                hope: RelationshipBand::Guarded,
                respect: RelationshipBand::Warming,
                intimacy: RelationshipBand::Guarded,
                attachment: RelationshipBand::Warming,
                updated_at_node: None,
            },
            history: Vec::new(),
            can_continue: true,
        }
    }

    fn result(node_id: &str) -> TurnResult {
        TurnResult {
            committed_node_id: node_id.into(),
            player_view: player_view(node_id),
        }
    }

    fn entry(
        id: &str,
        keywords: &[&str],
        tags: &[&str],
        required_flags: &[&str],
    ) -> WorldBookEntry {
        WorldBookEntry {
            id: id.into(),
            title: id.into(),
            content: format!("content for {id}"),
            keywords: keywords.iter().map(ToString::to_string).collect(),
            tags: tags.iter().map(ToString::to_string).collect(),
            required_flags: required_flags.iter().map(ToString::to_string).collect(),
        }
    }

    #[test]
    fn continue_allows_empty_input() {
        assert!(validate_turn_request(&request(TurnIntent::Continue, "")).is_ok());
    }

    #[test]
    fn speak_or_act_rejects_empty_input() {
        let failure = validate_turn_request(&request(TurnIntent::SpeakOrAct, " \n "))
            .expect_err("blank player input must be rejected");

        assert_eq!(failure.code, TurnFailureCode::InvalidInput);
        assert!(!failure.retryable);
        assert_eq!(
            failure.message,
            "input must not be empty for a speak_or_act turn"
        );
    }

    #[test]
    fn required_request_identifiers_must_not_be_blank() {
        for field in ["story_id", "branch_id", "expected_node_id", "action_id"] {
            let mut request = request(TurnIntent::Continue, "");
            match field {
                "story_id" => request.story_id = " ".into(),
                "branch_id" => request.branch_id = " ".into(),
                "expected_node_id" => request.expected_node_id = " ".into(),
                "action_id" => request.action_id = " ".into(),
                _ => unreachable!(),
            }

            let failure =
                validate_turn_request(&request).expect_err("blank identifier must be rejected");
            assert_eq!(failure.code, TurnFailureCode::InvalidInput);
            assert_eq!(failure.message, format!("{field} must not be empty"));
        }
    }

    #[test]
    fn regenerate_and_push_check_remain_valid_without_extra_rules() {
        assert!(validate_turn_request(&request(TurnIntent::Regenerate, "")).is_ok());
        assert!(validate_turn_request(&request(TurnIntent::PushCheck, "")).is_ok());
    }

    #[test]
    fn fake_provider_returns_fixtures_fifo() {
        let mut provider = FakeProvider::new([result("node_2"), result("node_3")]);
        let request = request(TurnIntent::Continue, "");

        assert_eq!(
            provider
                .complete_turn(&request)
                .expect("first response")
                .committed_node_id,
            "node_2"
        );
        assert_eq!(
            provider
                .complete_turn(&request)
                .expect("second response")
                .committed_node_id,
            "node_3"
        );
        assert!(matches!(
            provider.complete_turn(&request),
            Err(ProviderError::FixtureExhausted)
        ));
    }

    #[test]
    fn execution_validates_before_consuming_the_provider() {
        let mut provider = FakeProvider::new([result("node_2")]);
        let invalid = request(TurnIntent::SpeakOrAct, "");

        assert_eq!(
            execute_turn(&mut provider, &invalid)
                .expect_err("invalid request must fail before the provider")
                .code,
            TurnFailureCode::InvalidInput
        );

        let committed = execute_turn(
            &mut provider,
            &request(TurnIntent::SpeakOrAct, "I will return."),
        )
        .expect("the queued response must still be available");
        assert_eq!(committed.committed_node_id, "node_2");
    }

    #[test]
    fn execution_rejects_an_uncommitted_or_inconsistent_result() {
        let request = request(TurnIntent::Continue, "");

        let mut stale = FakeProvider::new([result("node_1")]);
        let stale_failure =
            execute_turn(&mut stale, &request).expect_err("old expected node is not a commit");
        assert_eq!(stale_failure.code, TurnFailureCode::InvalidModelOutput);

        let mut wrong_node = result("node_2");
        wrong_node.player_view.node_id = "node_other".into();
        let mut provider = FakeProvider::new([wrong_node]);
        let failure =
            execute_turn(&mut provider, &request).expect_err("view node must match committed node");
        assert_eq!(failure.code, TurnFailureCode::InvalidModelOutput);

        let mut wrong_story = result("node_2");
        wrong_story.player_view.story_id = "story_other".into();
        let mut provider = FakeProvider::new([wrong_story]);
        assert_eq!(
            execute_turn(&mut provider, &request)
                .expect_err("view story must match request")
                .code,
            TurnFailureCode::InvalidModelOutput
        );

        let mut wrong_branch = result("node_2");
        wrong_branch.player_view.branch_id = "branch_other".into();
        let mut provider = FakeProvider::new([wrong_branch]);
        assert_eq!(
            execute_turn(&mut provider, &request)
                .expect_err("view branch must match request")
                .code,
            TurnFailureCode::InvalidModelOutput
        );
    }

    #[test]
    fn execution_redacts_provider_details() {
        struct FailingProvider;

        impl TurnProvider for FailingProvider {
            fn complete_turn(
                &mut self,
                _request: &TurnRequest,
            ) -> Result<TurnResult, ProviderError> {
                Err(ProviderError::Upstream {
                    code: Some(openlapp::ErrorCode::HttpStatus),
                })
            }
        }

        let upstream = ProviderError::Upstream {
            code: Some(openlapp::ErrorCode::HttpStatus),
        };
        assert_eq!(upstream.to_string(), "LAPP chat request failed");

        let failure = execute_turn(&mut FailingProvider, &request(TurnIntent::Continue, ""))
            .expect_err("provider failure should be mapped");

        assert_eq!(failure.code, TurnFailureCode::ProviderUnavailable);
        assert_eq!(failure.message, "turn provider is unavailable");
        assert!(!failure.message.contains("secret"));
        assert!(failure.retryable);
    }

    #[test]
    fn world_book_selection_applies_flags_keywords_and_tags() {
        let entries = vec![
            entry("public_station", &["station"], &[], &[]),
            entry("flagged_station", &["station"], &[], &["clock_seen"]),
            entry("rain_lore", &[], &[" Weather "], &[]),
            entry("not_triggered", &["forest"], &["family"], &[]),
        ];
        let flags = BTreeSet::new();
        let tags = BTreeSet::from([String::from("weather")]);

        let selected =
            select_world_book_entries(&entries, &flags, &tags, "Return to the STATION", 8);
        assert_eq!(
            selected
                .iter()
                .map(|entry| entry.id.as_str())
                .collect::<Vec<_>>(),
            ["public_station", "rain_lore"]
        );

        let flags = BTreeSet::from([String::from("clock_seen")]);
        let selected =
            select_world_book_entries(&entries, &flags, &tags, "Return to the station", 8);
        assert_eq!(
            selected
                .iter()
                .map(|entry| entry.id.as_str())
                .collect::<Vec<_>>(),
            ["public_station", "flagged_station", "rain_lore"]
        );
    }

    #[test]
    fn world_book_selection_is_stable_deduplicated_and_limited() {
        let mut entries = vec![
            entry("first", &["rain"], &[], &[]),
            entry("duplicate", &["rain"], &[], &[]),
            entry("duplicate", &["rain"], &[], &[]),
        ];
        entries.extend(
            (0..MAX_WORLD_BOOK_ENTRIES + 3)
                .map(|index| entry(&format!("extra_{index}"), &["rain"], &[], &[])),
        );

        let selected =
            select_world_book_entries(&entries, &BTreeSet::new(), &BTreeSet::new(), "rain", 3);
        assert_eq!(
            selected
                .iter()
                .map(|entry| entry.id.as_str())
                .collect::<Vec<_>>(),
            ["first", "duplicate", "extra_0"]
        );

        let capped = select_world_book_entries(
            &entries,
            &BTreeSet::new(),
            &BTreeSet::new(),
            "rain",
            usize::MAX,
        );
        assert_eq!(capped.len(), MAX_WORLD_BOOK_ENTRIES);

        let repeated =
            select_world_book_entries(&entries, &BTreeSet::new(), &BTreeSet::new(), "rain", 3);
        assert_eq!(selected, repeated);
    }
}

#[cfg(test)]
mod persistent_turn_tests {
    use std::collections::{BTreeMap, VecDeque};

    use nana_domain::{
        ActionSuggestion, BeatKind, PlayerView, PresentationBeat, PresentationCharacter,
        PresentationScene, PresentationSnapshot, RelationshipAdjustment, RelationshipBand,
        RelationshipDimension, RelationshipView, RuntimeState, StateDelta, StateOp, StoryNode,
        TurnFailureCode, TurnIntent, TurnRequest,
    };
    use nana_store::{InMemoryStoryStore, StoryStore};

    use super::{
        ProviderError, TurnEngine, TurnPlan, TurnPlanProvider, TurnProjector, hash_runtime_state,
    };

    struct RecordingPlanProvider {
        responses: VecDeque<Result<TurnPlan, ProviderError>>,
        calls: usize,
    }

    impl RecordingPlanProvider {
        fn new(response: Result<TurnPlan, ProviderError>) -> Self {
            Self {
                responses: VecDeque::from([response]),
                calls: 0,
            }
        }
    }

    impl TurnPlanProvider for RecordingPlanProvider {
        fn plan_turn(
            &mut self,
            _request: &TurnRequest,
            _state: &RuntimeState,
        ) -> Result<TurnPlan, ProviderError> {
            self.calls += 1;
            self.responses
                .pop_front()
                .unwrap_or(Err(ProviderError::FixtureExhausted))
        }
    }

    struct RecordingProjector<'store> {
        store: &'store InMemoryStoryStore,
        calls: usize,
    }

    impl TurnProjector for RecordingProjector<'_> {
        fn project_committed_turn(&mut self, state: &RuntimeState, node: &StoryNode) -> PlayerView {
            self.calls += 1;

            let stored = self
                .store
                .load_state(&state.story_id, &state.current_branch)
                .expect("projection must run after append");
            assert_eq!(stored, *state);
            assert_eq!(node.id, state.current_node);

            PlayerView {
                // Deliberately wrong: the engine normalizes commit identity.
                story_id: "untrusted_story".into(),
                node_id: "untrusted_node".into(),
                branch_id: "untrusted_branch".into(),
                scene_id: "station".into(),
                scene_title: "Station".into(),
                character_name: "Nana".into(),
                character_expression: node.presentation.character.expression.clone(),
                character_pose: node.presentation.character.pose.clone(),
                beats: node.presentation.beats.clone(),
                suggestions: node.presentation.suggestions.clone(),
                inventory: Vec::new(),
                knowledge: Vec::new(),
                promises: Vec::new(),
                relationship: RelationshipView {
                    affinity: RelationshipBand::Warming,
                    trust: RelationshipBand::Guarded,
                    hope: RelationshipBand::Guarded,
                    respect: RelationshipBand::Warming,
                    intimacy: RelationshipBand::Guarded,
                    attachment: RelationshipBand::Warming,
                    updated_at_node: Some(node.id.clone()),
                },
                history: Vec::new(),
                can_continue: true,
            }
        }
    }

    fn state(node: &str, branch: &str) -> RuntimeState {
        RuntimeState {
            story_id: "story_1".into(),
            current_node: node.into(),
            current_branch: branch.into(),
            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 {
        StoryNode {
            id: id.into(),
            story_id: "story_1".into(),
            branch_id: branch.into(),
            parent_id: parent_id.map(Into::into),
            action_id: format!("action_{id}"),
            user_input: String::new(),
            presentation: PresentationSnapshot::default(),
            delta: StateDelta { ops: Vec::new() },
            state_hash: hash_runtime_state(&state(id, branch)).expect("serializable test state"),
        }
    }

    fn seeded_store() -> InMemoryStoryStore {
        let store = InMemoryStoryStore::new();
        store
            .append_node(
                &node("node_1", None, "branch_main"),
                &state("node_1", "branch_main"),
            )
            .expect("seed root");
        store
    }

    fn request(expected_node_id: &str) -> TurnRequest {
        TurnRequest {
            story_id: "story_1".into(),
            branch_id: "branch_main".into(),
            expected_node_id: expected_node_id.into(),
            action_id: "action_2".into(),
            intent: TurnIntent::SpeakOrAct,
            input: "I will return before dawn.".into(),
        }
    }

    fn plan(node_id: &str, delta: StateDelta) -> TurnPlan {
        TurnPlan {
            committed_node_id: node_id.into(),
            presentation: PresentationSnapshot {
                scene: PresentationScene {
                    id: "station".into(),
                    title: "Station".into(),
                },
                character: PresentationCharacter {
                    id: "nana".into(),
                    name: "Nana".into(),
                    expression: Some("guarded".into()),
                    pose: Some("holding_coat".into()),
                },
                beats: vec![PresentationBeat {
                    id: "beat_1".into(),
                    kind: BeatKind::Dialogue,
                    speaker: Some("Nana".into()),
                    text: "Then I will wait.".into(),
                    visual: None,
                }],
                suggestions: vec![ActionSuggestion {
                    id: "suggestion_1".into(),
                    label: "Promise".into(),
                    draft: "I promise.".into(),
                }],
                can_continue: true,
            },
            delta,
        }
    }

    fn projector(store: &InMemoryStoryStore) -> RecordingProjector<'_> {
        RecordingProjector { store, calls: 0 }
    }

    #[test]
    fn successful_turn_commits_before_projecting() {
        let store = seeded_store();
        let delta = StateDelta {
            ops: vec![StateOp::SetWorldFlag {
                key: "promise_spoken".into(),
                value: true,
            }],
        };
        let mut engine = TurnEngine::new(
            &store,
            RecordingPlanProvider::new(Ok(plan("node_2", delta))),
            projector(&store),
        );

        let result = engine.submit_turn(&request("node_1")).expect("turn");

        assert_eq!(result.committed_node_id, "node_2");
        assert_eq!(result.player_view.story_id, "story_1");
        assert_eq!(result.player_view.node_id, "node_2");
        assert_eq!(result.player_view.branch_id, "branch_main");
        assert_eq!(result.player_view.suggestions.len(), 1);
        assert_eq!(engine.provider().calls, 1);
        assert_eq!(engine.projector().calls, 1);

        let committed = store
            .load_state("story_1", "branch_main")
            .expect("committed state");
        assert_eq!(committed.current_node, "node_2");
        assert_eq!(committed.world_flags.get("promise_spoken"), Some(&true));
    }

    #[test]
    fn stale_request_does_not_call_provider_or_move_head() {
        let store = seeded_store();
        let mut engine = TurnEngine::new(
            &store,
            RecordingPlanProvider::new(Ok(plan("node_2", StateDelta { ops: Vec::new() }))),
            projector(&store),
        );

        let failure = engine
            .submit_turn(&request("node_stale"))
            .expect_err("stale request");

        assert_eq!(failure.code, TurnFailureCode::StaleNode);
        assert!(failure.retryable);
        assert_eq!(engine.provider().calls, 0);
        assert_eq!(engine.projector().calls, 0);
        assert_eq!(
            store.branch_head("story_1", "branch_main").expect("head"),
            Some("node_1".into())
        );
    }

    #[test]
    fn reducer_failure_is_redacted_and_does_not_save() {
        let store = seeded_store();
        let delta = StateDelta {
            ops: vec![StateOp::AdjustRelationship {
                from: "nana".into(),
                to: "player".into(),
                adjustment: RelationshipAdjustment {
                    dimension: RelationshipDimension::Trust,
                    delta: 9,
                    cause: "secret model reasoning".into(),
                    judgment_rule: Some("secret.rule".into()),
                },
            }],
        };
        let mut engine = TurnEngine::new(
            &store,
            RecordingPlanProvider::new(Ok(plan("node_2", delta))),
            projector(&store),
        );

        let failure = engine
            .submit_turn(&request("node_1"))
            .expect_err("invalid delta");

        assert_eq!(failure.code, TurnFailureCode::InvalidModelOutput);
        assert_eq!(failure.message, "turn plan could not be applied");
        assert!(!failure.message.contains("secret"));
        assert_eq!(engine.provider().calls, 1);
        assert_eq!(engine.projector().calls, 0);
        assert_eq!(
            store.branch_head("story_1", "branch_main").expect("head"),
            Some("node_1".into())
        );
    }

    #[test]
    fn provider_failure_is_redacted_and_does_not_save() {
        let store = seeded_store();
        let mut engine = TurnEngine::new(
            &store,
            RecordingPlanProvider::new(Err(ProviderError::Upstream {
                code: Some(openlapp::ErrorCode::HttpStatus),
            })),
            projector(&store),
        );

        let failure = engine
            .submit_turn(&request("node_1"))
            .expect_err("provider failure");

        assert_eq!(failure.code, TurnFailureCode::ProviderUnavailable);
        assert_eq!(failure.message, "turn provider is unavailable");
        assert!(!failure.message.contains("secret"));
        assert_eq!(engine.projector().calls, 0);
        assert_eq!(
            store.branch_head("story_1", "branch_main").expect("head"),
            Some("node_1".into())
        );
    }

    #[test]
    fn append_failure_does_not_project_or_move_requested_branch() {
        let store = seeded_store();
        store
            .append_node(
                &node("node_duplicate", Some("node_1"), "branch_other"),
                &state("node_duplicate", "branch_other"),
            )
            .expect("seed duplicate id on another branch");
        let mut engine = TurnEngine::new(
            &store,
            RecordingPlanProvider::new(Ok(plan("node_duplicate", StateDelta { ops: Vec::new() }))),
            projector(&store),
        );

        let failure = engine
            .submit_turn(&request("node_1"))
            .expect_err("duplicate node append");

        assert_eq!(failure.code, TurnFailureCode::Internal);
        assert_eq!(failure.message, "turn could not be committed");
        assert_eq!(engine.projector().calls, 0);
        assert_eq!(
            store.branch_head("story_1", "branch_main").expect("head"),
            Some("node_1".into())
        );
    }

    #[test]
    fn turn_plan_is_non_view_provider_output_and_state_hash_is_stable() {
        fn provider_output(
            provider: &mut impl TurnPlanProvider,
            request: &TurnRequest,
            state: &RuntimeState,
        ) -> TurnPlan {
            provider.plan_turn(request, state).expect("turn plan")
        }

        let current = state("node_1", "branch_main");
        let expected = hash_runtime_state(&current).expect("hash");
        assert_eq!(hash_runtime_state(&current).expect("repeat hash"), expected);

        let mut provider =
            RecordingPlanProvider::new(Ok(plan("node_2", StateDelta { ops: Vec::new() })));
        let output = provider_output(&mut provider, &request("node_1"), &current);
        assert_eq!(output.committed_node_id, "node_2");
        assert_eq!(output.presentation.beats.len(), 1);
        assert_eq!(output.presentation.suggestions.len(), 1);
    }
}
