use super::*;

#[test]
fn assembler_ignores_streamed_text_without_durable_output() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TextDelta {
        content: "streamed but not committed".to_string(),
    });
    assembler.push(&SessionEvent::Done);

    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(
        out.outcome,
        TurnOutcome::Finished(TurnFinish::AssistantMessage {
            text: String::new()
        })
    );
    assert!(out.assistant_output.safe_text.is_empty());
    assert!(out.assistant_output.raw_text.is_empty());
    assert_eq!(out.assistant_output.state, OutputState::EmptyOutput);
}

#[test]
fn cancelled_assembler_with_only_streamed_text_has_empty_assistant_output() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TextDelta {
        content: "partial answer".to_string(),
    });

    let out = assembler.finish(
        default_state().to_snapshot(),
        true,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(out.outcome, TurnOutcome::Stopped(TurnStop::Cancelled));
    assert!(out.assistant_output.safe_text.is_empty());
    assert!(out.assistant_output.raw_text.is_empty());
    assert_eq!(out.assistant_output.state, OutputState::EmptyOutput);
}

#[test]
fn assembler_preserves_explicit_assistant_message_outcome() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TurnOutcome {
        outcome: TurnOutcome::Finished(TurnFinish::AssistantMessage {
            text: "first\n\nsecond".to_string(),
        }),
    });
    assembler.push(&SessionEvent::Done);

    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(
        out.outcome,
        TurnOutcome::Finished(TurnFinish::AssistantMessage {
            text: "first\n\nsecond".to_string()
        })
    );
    assert_eq!(out.assistant_output.safe_text, "first\n\nsecond");
}

#[test]
fn assembler_uses_assistant_message_outcome_without_recovery_issue_when_no_streamed_prose() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TurnOutcome {
        outcome: TurnOutcome::Finished(TurnFinish::AssistantMessage {
            text: "settled answer".to_string(),
        }),
    });
    assembler.push(&SessionEvent::Done);

    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(
        out.outcome,
        TurnOutcome::Finished(TurnFinish::AssistantMessage {
            text: "settled answer".to_string()
        })
    );
    assert_eq!(out.assistant_output.safe_text, "settled answer");
    assert!(
        out.errors
            .iter()
            .all(|issue| issue.code.as_deref() != Some("assistant_output_recovered_from_state"))
    );
}

#[test]
fn assembler_uses_final_value_for_assistant_output() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TurnOutcome {
        outcome: TurnOutcome::Finished(TurnFinish::FinalValue {
            value: serde_json::json!({ "ok": true }),
        }),
    });
    assembler.push(&SessionEvent::Done);

    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(
        out.outcome,
        TurnOutcome::Finished(TurnFinish::FinalValue {
            value: serde_json::json!({ "ok": true })
        })
    );
    assert_eq!(out.assistant_output.safe_text, "{\n  \"ok\": true\n}");
}

#[test]
fn assembler_uses_tool_value_for_assistant_output() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TurnOutcome {
        outcome: TurnOutcome::Finished(TurnFinish::ToolValue {
            tool_name: "finish".to_string(),
            value: serde_json::json!("done"),
        }),
    });
    assembler.push(&SessionEvent::Done);

    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );

    assert_eq!(
        out.outcome,
        TurnOutcome::Finished(TurnFinish::ToolValue {
            tool_name: "finish".to_string(),
            value: serde_json::json!("done")
        })
    );
    assert_eq!(out.assistant_output.safe_text, "done");
}

#[test]
fn assembler_falls_back_to_last_assistant_message_when_stream_output_is_empty() {
    let mut state = default_state();
    append_message(
        &mut state,
        Message {
            id: "m0".to_string(),
            role: MessageRole::Assistant,
            parts: vec![Part {
                id: "m0.p0".to_string(),
                kind: PartKind::Prose,
                content: "stored".to_string(),
                attachment: None,
                tool_call_id: None,
                tool_name: None,
                tool_replay: None,
                prune_state: PruneState::Intact,
                reasoning_meta: None,
                response_meta: None,
            }]
            .into(),
            origin: None,
        },
    );
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::Done);
    let out = assembler.finish(
        state.to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Finished(_) | TurnOutcome::AgentFrameSwitch { .. }
    ));
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Finished(TurnFinish::AssistantMessage { .. })
    ));
    assert_eq!(out.assistant_output.safe_text, "stored");
    assert_eq!(out.assistant_output.raw_text, "stored");
    assert_eq!(out.assistant_output.state, OutputState::Usable);
}

#[test]
fn interrupted_assembler_does_not_reuse_assistant_before_latest_user_message() {
    let mut state = default_state();
    append_message(
        &mut state,
        Message {
            id: "a0".to_string(),
            role: MessageRole::Assistant,
            parts: vec![Part {
                id: "a0.p0".to_string(),
                kind: PartKind::Prose,
                content: "previous assistant answer".to_string(),
                attachment: None,
                tool_call_id: None,
                tool_name: None,
                tool_replay: None,
                prune_state: PruneState::Intact,
                reasoning_meta: None,
                response_meta: None,
            }]
            .into(),
            origin: None,
        },
    );
    append_message(
        &mut state,
        Message {
            id: "u1".to_string(),
            role: MessageRole::User,
            parts: vec![Part {
                id: "u1.p0".to_string(),
                kind: PartKind::Text,
                content: "new prompt".to_string(),
                attachment: None,
                tool_call_id: None,
                tool_name: None,
                tool_replay: None,
                prune_state: PruneState::Intact,
                reasoning_meta: None,
                response_meta: None,
            }]
            .into(),
            origin: None,
        },
    );

    let out = TurnAssembler::default().finish(
        state.to_snapshot(),
        true,
        None,
        &TerminationPolicy::default(),
    );

    assert!(matches!(
        &out.outcome,
        TurnOutcome::Stopped(TurnStop::Cancelled)
    ));
    assert!(out.assistant_output.safe_text.is_empty());
    assert!(out.assistant_output.raw_text.is_empty());
}

#[test]
fn assembler_prefers_state_output_when_streamed_text_is_a_truncated_prefix() {
    let mut state = default_state();
    append_message(
        &mut state,
        Message {
            id: "m0".to_string(),
            role: MessageRole::Assistant,
            parts: vec![Part {
                id: "m0.p0".to_string(),
                kind: PartKind::Prose,
                content: "You graduated with a degree in Business Administration.".to_string(),
                attachment: None,
                tool_call_id: None,
                tool_name: None,
                tool_replay: None,
                prune_state: PruneState::Intact,
                reasoning_meta: None,
                response_meta: None,
            }]
            .into(),
            origin: None,
        },
    );
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TextDelta {
        content: "You graduated with a degree in Business".to_string(),
    });
    assembler.push(&SessionEvent::Done);
    let out = assembler.finish(
        state.to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );
    assert_eq!(
        out.assistant_output.safe_text,
        "You graduated with a degree in Business Administration."
    );
    assert_eq!(
        out.assistant_output.raw_text,
        "You graduated with a degree in Business Administration."
    );
    assert_eq!(out.assistant_output.state, OutputState::Usable);
}

#[test]
fn assembler_state_output_excludes_tool_call_payload() {
    // Regression: codex commits an assistant message containing a prose
    // part followed by a tool-call part whose `content` is the raw JSON
    // arguments. On interrupt the assembler falls back to the last
    // assistant message's parts; concatenating EVERY part's content
    // leaks the tool-call JSON into safe_text and the UI then renders it
    // as a literal AssistantText block. Only Text/Prose/Image parts
    // should appear in safe_text.
    let mut state = default_state();
    append_message(
        &mut state,
        Message {
            id: "m0".to_string(),
            role: MessageRole::Assistant,
            parts: vec![
                Part {
                    id: "m0.p0".to_string(),
                    kind: PartKind::Prose,
                    content: "Searching for the relevant code.".to_string(),
                    attachment: None,
                    tool_call_id: None,
                    tool_name: None,
                    tool_replay: None,
                    prune_state: PruneState::Intact,
                    reasoning_meta: None,
                    response_meta: None,
                },
                Part {
                    id: "m0.p1".to_string(),
                    kind: PartKind::ToolCall,
                    content:
                        "{\"tool_calls\":[{\"tool\":\"grep\",\"parameters\":{\"query\":\"x\"}}]}"
                            .to_string(),
                    attachment: None,
                    tool_call_id: Some("tc1".to_string()),
                    tool_name: Some("batch".to_string()),
                    tool_replay: None,
                    prune_state: PruneState::Intact,
                    reasoning_meta: None,
                    response_meta: None,
                },
            ]
            .into(),
            origin: None,
        },
    );
    let assembler = TurnAssembler::default();
    let out = assembler.finish(
        state.to_snapshot(),
        true,
        None,
        &TerminationPolicy::default(),
    );
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Stopped(TurnStop::Cancelled)
    ));
    assert_eq!(
        out.assistant_output.safe_text,
        "Searching for the relevant code."
    );
    assert!(!out.assistant_output.raw_text.contains("tool_calls"));
}

#[test]
fn assembler_marks_tool_failure() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::ToolCall {
        call_id: Some("tc1".to_string()),
        name: "x".to_string(),
        args: serde_json::json!({}),
        output: crate::ToolCallOutput::failure(crate::ToolFailure::tool(
            crate::ToolFailureClass::Execution,
            "tool_error",
            serde_json::json!({"error": true}).to_string(),
        )),
        duration_ms: 1,
    });
    assembler.push(&SessionEvent::Error {
        message: "tool failed".to_string(),
        envelope: None,
    });
    assembler.push(&SessionEvent::Done);
    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );
    assert!(matches!(&out.outcome, TurnOutcome::Stopped(_)));
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Stopped(TurnStop::ToolFailure)
    ));
    assert_eq!(out.tool_calls.len(), 1);
}

#[test]
fn assembler_marks_missing_done_as_failure() {
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::TextDelta {
        content: "partial".to_string(),
    });
    let out = assembler.finish(
        default_state().to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );
    assert!(matches!(&out.outcome, TurnOutcome::Stopped(_)));
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Stopped(TurnStop::RuntimeError)
    ));
}

#[test]
fn assembler_detects_max_turn_message() {
    let mut state = default_state();
    append_message(
        &mut state,
        Message {
            id: "m0".to_string(),
            role: MessageRole::System,
            parts: vec![Part {
                id: "m0.p0".to_string(),
                kind: PartKind::Text,
                content: "Turn limit reached (5).".to_string(),
                attachment: None,
                tool_call_id: None,
                tool_name: None,
                tool_replay: None,
                prune_state: PruneState::Intact,
                reasoning_meta: None,
                response_meta: None,
            }]
            .into(),
            origin: None,
        },
    );
    let mut assembler = TurnAssembler::default();
    assembler.push(&SessionEvent::Done);
    let out = assembler.finish(
        state.to_snapshot(),
        false,
        None,
        &TerminationPolicy::default(),
    );
    assert!(matches!(
        &out.outcome,
        TurnOutcome::Stopped(TurnStop::MaxTurns)
    ));
}

#[test]
fn output_state_empty_output() {
    assert_eq!(classify_output_state("", "", &[]), OutputState::EmptyOutput);
}

#[test]
fn output_state_traceback_only() {
    let raw = "Runtime error: Traceback (most recent call last):\nFile \"frame_1.py\", line 2, in <module>\nNameError: name 'now' is not defined";
    assert_eq!(
        classify_output_state(raw, "", &[]),
        OutputState::TracebackOnly
    );
}

#[test]
fn output_state_recovered_from_error() {
    let issues = vec![TurnIssue {
        kind: "runtime".to_string(),
        code: Some("example".to_string()),
        terminal_reason: None,
        message: "something failed".to_string(),
        raw: None,
        retryable: None,
        provider_failure_kind: None,
    }];
    assert_eq!(
        classify_output_state("raw", "usable", &issues),
        OutputState::RecoveredFromError
    );
}

#[tokio::test]
async fn normalize_items_merges_adjacent_text_items() {
    let items = vec![
        InputItem::Text {
            text: "before ".to_string(),
        },
        InputItem::Text {
            text: "[file: host-prepared.txt]".to_string(),
        },
    ];
    let out = normalize_input_items(
        &items,
        &HashMap::new(),
        &crate::InMemoryAttachmentStore::new(),
    )
    .await
    .expect("normalized");
    assert_eq!(out.len(), 1);
    match &out[0] {
        NormalizedItem::Text(text) => {
            assert_eq!(text, "before [file: host-prepared.txt]");
        }
        _ => panic!("expected merged text item"),
    }
}
