//! Tests for [`super::apply`].
//!
//! Moved from `apply.rs` to keep the operation-apply dispatcher focused on
//! production logic. Tests retain full access to private helpers via
//! `use super::apply::*;`.

use super::apply::*;
use crate::model::block::{Block, BlockId};
use crate::model::document::Document;
use crate::model::inline::{Mark, TextSpan};
use crate::op::operation::Operation;

fn doc_with_block(id: &str, text: &str) -> Document {
    let mut doc = Document::new();
    let mut block = Block::new_paragraph(BlockId::new(id));
    if !text.is_empty() {
        block.content = vec![TextSpan::plain(text)];
    }
    doc.insert_block(block, 0);
    doc
}

#[test]
fn insert_text_empty_block() {
    let mut doc = doc_with_block("b1", "");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::InsertTextAt {
            block_id: BlockId::new("b1"),
            offset: 0,
            text: "hello".into(),
            marks: vec![],
        },
    )
    .unwrap();
    assert_eq!(doc.plain_text(&BlockId::new("b1")), "hello");
}

#[test]
fn insert_text_middle() {
    let mut doc = doc_with_block("b1", "helo");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::InsertTextAt {
            block_id: BlockId::new("b1"),
            offset: 3,
            text: "l".into(),
            marks: vec![],
        },
    )
    .unwrap();
    assert_eq!(doc.plain_text(&BlockId::new("b1")), "hello");
}

#[test]
fn delete_range() {
    let mut doc = doc_with_block("b1", "hello world");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::DeleteRange {
            block_id: BlockId::new("b1"),
            start: 5,
            end: 11,
        },
    )
    .unwrap();
    assert_eq!(doc.plain_text(&BlockId::new("b1")), "hello");
}

#[test]
fn delete_range_clamps_surrogate_pairs() {
    let mut doc = doc_with_block("b1", "a😀b");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::DeleteRange {
            block_id: BlockId::new("b1"),
            start: 2,
            end: 3,
        },
    )
    .unwrap();

    assert_eq!(doc.plain_text(&BlockId::new("b1")), "ab");
}

#[test]
fn split_block() {
    let mut doc = doc_with_block("b1", "hello world");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::SplitBlock {
            block_id: BlockId::new("b1"),
            offset: 5,
            new_block_id: BlockId::new("b2"),
        },
    )
    .unwrap();
    assert_eq!(doc.plain_text(&BlockId::new("b1")), "hello");
    assert_eq!(doc.plain_text(&BlockId::new("b2")), " world");
    assert_eq!(doc.root_block_count(), 2);
}

#[test]
fn merge_blocks() {
    let mut doc = Document::new();
    let mut b1 = Block::new_paragraph(BlockId::new("b1"));
    b1.content = vec![TextSpan::plain("hello")];
    let mut b2 = Block::new_paragraph(BlockId::new("b2"));
    b2.content = vec![TextSpan::plain(" world")];
    doc.insert_block(b1, 0);
    doc.insert_block(b2, 1);

    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::MergeBlock {
            target_id: BlockId::new("b1"),
            source_id: BlockId::new("b2"),
        },
    )
    .unwrap();
    assert_eq!(doc.plain_text(&BlockId::new("b1")), "hello world");
    assert_eq!(doc.root_block_count(), 1);
}

#[test]
fn toggle_mark_applies_bold() {
    let mut doc = doc_with_block("b1", "hello");
    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::ToggleMark {
            block_id: BlockId::new("b1"),
            start: 0,
            end: 5,
            mark_type: "bold".into(),
        },
    )
    .unwrap();
    let block = doc.get_block(&BlockId::new("b1")).unwrap();
    assert!(block.content[0].has_mark("bold"));
}

#[test]
fn toggle_mark_removes_bold() {
    let mut doc = Document::new();
    let mut block = Block::new_paragraph(BlockId::new("b1"));
    block.content = vec![TextSpan::with_marks("hello", vec![Mark::bold()])];
    doc.insert_block(block, 0);

    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::ToggleMark {
            block_id: BlockId::new("b1"),
            start: 0,
            end: 5,
            mark_type: "bold".into(),
        },
    )
    .unwrap();
    let block = doc.get_block(&BlockId::new("b1")).unwrap();
    assert!(!block.content[0].has_mark("bold"));
}

#[test]
fn set_mark_applies_color_attrs() {
    let mut doc = doc_with_block("b1", "hello");
    let mut sel = None;
    let mut style = serde_json::Map::new();
    style.insert("color".into(), serde_json::Value::String("#ff0000".into()));
    let mut attrs = serde_json::Map::new();
    attrs.insert("style".into(), serde_json::Value::Object(style));

    apply_op(
        &mut doc,
        &mut sel,
        &Operation::SetMark {
            block_id: BlockId::new("b1"),
            start: 0,
            end: 5,
            mark: Mark::with_attrs("color", attrs),
        },
    )
    .unwrap();

    let block = doc.get_block(&BlockId::new("b1")).unwrap();
    assert_eq!(block.content.len(), 1);
    assert_eq!(block.content[0].marks[0].ty, "color");
    assert_eq!(
        block.content[0].marks[0]
            .style()
            .and_then(|s| s.get("color"))
            .and_then(serde_json::Value::as_str),
        Some("#ff0000")
    );
}

#[test]
fn clear_mark_removes_color_but_preserves_other_marks() {
    let mut color_style = serde_json::Map::new();
    color_style.insert("color".into(), serde_json::Value::String("#ff0000".into()));
    let mut color_attrs = serde_json::Map::new();
    color_attrs.insert("style".into(), serde_json::Value::Object(color_style));

    let mut doc = Document::new();
    let mut block = Block::new_paragraph(BlockId::new("b1"));
    block.content = vec![TextSpan::with_marks(
        "hello",
        vec![Mark::bold(), Mark::with_attrs("color", color_attrs)],
    )];
    doc.insert_block(block, 0);

    let mut sel = None;
    apply_op(
        &mut doc,
        &mut sel,
        &Operation::ClearMark {
            block_id: BlockId::new("b1"),
            start: 0,
            end: 5,
            mark_type: "color".into(),
        },
    )
    .unwrap();

    let block = doc.get_block(&BlockId::new("b1")).unwrap();
    assert_eq!(block.content.len(), 1);
    assert!(block.content[0].has_mark("bold"));
    assert!(!block.content[0].has_mark("color"));
}
