//! Domain tests for the [`Memory`] entity and its value objects (LOT A, étage 1):
//! [`MemorySlug`] validation, `[[slug]]` link scanning, frontmatter serde
//! round-trip, and the derived index entry.

use domain::{DomainError, MarkdownDoc, Memory, MemoryFrontmatter, MemorySlug, MemoryType};

fn fm(slug: &str, description: &str, ty: MemoryType) -> MemoryFrontmatter {
    MemoryFrontmatter {
        name: MemorySlug::new(slug).unwrap(),
        description: description.to_string(),
        r#type: ty,
    }
}

fn note(slug: &str, body: &str) -> Memory {
    Memory::new(
        fm(slug, "a hook", MemoryType::Project),
        MarkdownDoc::new(body),
    )
    .unwrap()
}

// ---------------------------------------------------------------------------
// MemorySlug
// ---------------------------------------------------------------------------

#[test]
fn slug_accepts_kebab_case() {
    assert_eq!(
        MemorySlug::new("my-note-42").unwrap().as_str(),
        "my-note-42"
    );
    assert!(MemorySlug::new("abc").is_ok());
    assert!(MemorySlug::new("a-b-c-1-2-3").is_ok());
}

#[test]
fn slug_rejects_uppercase() {
    assert!(matches!(
        MemorySlug::new("MyNote").unwrap_err(),
        DomainError::InvalidSlug { .. }
    ));
}

#[test]
fn slug_rejects_spaces() {
    assert!(matches!(
        MemorySlug::new("my note").unwrap_err(),
        DomainError::InvalidSlug { .. }
    ));
}

#[test]
fn slug_rejects_dot_and_traversal() {
    assert!(matches!(
        MemorySlug::new("..").unwrap_err(),
        DomainError::InvalidSlug { .. }
    ));
    assert!(matches!(
        MemorySlug::new("a.b").unwrap_err(),
        DomainError::InvalidSlug { .. }
    ));
}

#[test]
fn slug_rejects_empty() {
    assert!(matches!(
        MemorySlug::new("").unwrap_err(),
        DomainError::InvalidSlug { .. }
    ));
}

#[test]
fn slug_rejects_underscore_and_slash() {
    assert!(MemorySlug::new("a_b").is_err());
    assert!(MemorySlug::new("a/b").is_err());
}

// ---------------------------------------------------------------------------
// Memory invariants
// ---------------------------------------------------------------------------

#[test]
fn memory_rejects_empty_description() {
    let r = Memory::new(
        fm("ok-slug", "", MemoryType::User),
        MarkdownDoc::new("body"),
    );
    assert!(matches!(r.unwrap_err(), DomainError::EmptyField { .. }));
}

#[test]
fn memory_rejects_empty_body() {
    let r = Memory::new(
        fm("ok-slug", "hook", MemoryType::User),
        MarkdownDoc::new(""),
    );
    assert!(matches!(r.unwrap_err(), DomainError::EmptyField { .. }));
}

// ---------------------------------------------------------------------------
// outgoing_links
// ---------------------------------------------------------------------------

#[test]
fn outgoing_links_none() {
    assert!(note("n", "plain body, no links")
        .outgoing_links()
        .is_empty());
}

#[test]
fn outgoing_links_multiple() {
    let m = note("n", "see [[alpha]] and also [[beta-2]] here");
    let targets: Vec<_> = m
        .outgoing_links()
        .into_iter()
        .map(|l| l.target.as_str().to_string())
        .collect();
    assert_eq!(targets, vec!["alpha".to_string(), "beta-2".to_string()]);
}

#[test]
fn outgoing_links_preserves_duplicates() {
    let m = note("n", "[[alpha]] then again [[alpha]]");
    assert_eq!(m.outgoing_links().len(), 2);
}

#[test]
fn outgoing_links_skips_invalid_targets() {
    // `[[Not Valid]]` is not a kebab-case slug -> skipped.
    let m = note("n", "[[Not Valid]] but [[good-one]]");
    let targets: Vec<_> = m
        .outgoing_links()
        .into_iter()
        .map(|l| l.target.as_str().to_string())
        .collect();
    assert_eq!(targets, vec!["good-one".to_string()]);
}

#[test]
fn outgoing_links_ignores_unterminated() {
    let m = note("n", "dangling [[oops without close");
    assert!(m.outgoing_links().is_empty());
}

// ---------------------------------------------------------------------------
// index_entry
// ---------------------------------------------------------------------------

#[test]
fn index_entry_carries_slug_hook_and_type() {
    let m = Memory::new(
        fm("topic-x", "a useful hook", MemoryType::Feedback),
        MarkdownDoc::new("body"),
    )
    .unwrap();
    let e = m.index_entry();
    assert_eq!(e.slug.as_str(), "topic-x");
    assert_eq!(e.hook, "a useful hook");
    assert_eq!(e.title, "topic-x");
    assert_eq!(e.r#type, MemoryType::Feedback);
}

// ---------------------------------------------------------------------------
// Frontmatter serde round-trip (the `type` field name + camelCase variants).
// ---------------------------------------------------------------------------

#[test]
fn frontmatter_serde_roundtrip() {
    let original = fm("round-trip", "desc", MemoryType::Reference);
    let json = serde_json::to_string(&original).unwrap();
    // `type` field name (not `kind`), camelCase enum value.
    assert!(json.contains("\"type\":\"reference\""));
    assert!(json.contains("\"name\":\"round-trip\""));
    let back: MemoryFrontmatter = serde_json::from_str(&json).unwrap();
    assert_eq!(back, original);
}

#[test]
fn memory_type_serde_is_camel_case() {
    assert_eq!(
        serde_json::to_string(&MemoryType::User).unwrap(),
        "\"user\""
    );
    assert_eq!(
        serde_json::to_string(&MemoryType::Project).unwrap(),
        "\"project\""
    );
}
