ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
use std::path::Path;

use crate::change_repository::FsChangeRepository;

use super::list_ready_tasks_across_changes;

fn write(path: impl AsRef<Path>, contents: &str) {
    let path = path.as_ref();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("parent dirs should exist");
    }
    std::fs::write(path, contents).expect("test fixture should write");
}

fn make_ready_change(root: &Path, id: &str) {
    write(
        root.join(".ito/changes").join(id).join("proposal.md"),
        "## Why\nfixture\n\n## What Changes\n- fixture\n\n## Impact\n- fixture\n",
    );
    write(
        root.join(".ito/changes")
            .join(id)
            .join("specs")
            .join("alpha")
            .join("spec.md"),
        "## ADDED Requirements\n\n### Requirement: Fixture\nFixture requirement.\n\n#### Scenario: Works\n- **WHEN** fixture runs\n- **THEN** it is ready\n",
    );
    write(
        root.join(".ito/changes").join(id).join("tasks.md"),
        "## 1. Implementation\n- [ ] 1.1 pending\n",
    );
}

fn make_complete_change(root: &Path, id: &str) {
    write(
        root.join(".ito/changes").join(id).join("proposal.md"),
        "## Why\nfixture\n\n## What Changes\n- fixture\n\n## Impact\n- fixture\n",
    );
    write(
        root.join(".ito/changes")
            .join(id)
            .join("specs")
            .join("alpha")
            .join("spec.md"),
        "## ADDED Requirements\n\n### Requirement: Fixture\nFixture requirement.\n\n#### Scenario: Works\n- **WHEN** fixture runs\n- **THEN** it is ready\n",
    );
    write(
        root.join(".ito/changes").join(id).join("tasks.md"),
        "## 1. Implementation\n- [x] 1.1 done\n",
    );
}

#[test]
fn returns_ready_tasks_for_ready_changes() {
    let repo = tempfile::tempdir().expect("repo tempdir");
    let ito_path = repo.path().join(".ito");
    make_ready_change(repo.path(), "000-01_alpha");
    make_complete_change(repo.path(), "000-02_beta");

    let change_repo = FsChangeRepository::new(&ito_path);
    let ready =
        list_ready_tasks_across_changes(&change_repo, &ito_path).expect("ready task listing");

    assert_eq!(ready.len(), 1);
    assert_eq!(ready[0].change_id, "000-01_alpha");
    assert_eq!(ready[0].ready_tasks.len(), 1);
    assert_eq!(ready[0].ready_tasks[0].id, "1.1");
}

#[test]
fn returns_empty_when_no_ready_tasks_exist() {
    let repo = tempfile::tempdir().expect("repo tempdir");
    let ito_path = repo.path().join(".ito");
    make_complete_change(repo.path(), "000-01_alpha");

    let change_repo = FsChangeRepository::new(&ito_path);
    let ready =
        list_ready_tasks_across_changes(&change_repo, &ito_path).expect("ready task listing");

    assert!(ready.is_empty());
}

#[test]
fn read_tasks_markdown_returns_contents_for_existing_file() {
    let repo = tempfile::tempdir().expect("repo tempdir");
    let ito_path = repo.path().join(".ito");
    let change_id = "000-01_alpha";
    let tasks_content = "## 1. Implementation\n- [ ] 1.1 pending\n";
    write(
        ito_path.join("changes").join(change_id).join("tasks.md"),
        tasks_content,
    );

    let result = super::read_tasks_markdown(&ito_path, change_id).expect("should read tasks.md");
    assert_eq!(result, tasks_content);
}

#[test]
fn read_tasks_markdown_returns_error_for_missing_file() {
    let repo = tempfile::tempdir().expect("repo tempdir");
    let ito_path = repo.path().join(".ito");

    let result = super::read_tasks_markdown(&ito_path, "nonexistent-change");
    assert!(result.is_err(), "should fail for missing tasks.md");
    let err = result.unwrap_err();
    let msg = err.to_string();
    assert!(
        msg.contains("tasks.md"),
        "error should mention tasks.md, got: {msg}"
    );
}

#[test]
fn read_tasks_markdown_rejects_traversal_like_change_id() {
    let repo = tempfile::tempdir().expect("repo tempdir");
    let ito_path = repo.path().join(".ito");

    let result = super::read_tasks_markdown(&ito_path, "../escape");
    assert!(result.is_err(), "traversal-like ids should fail");
}