//! Test result persistence — writes structured test results into story markdown files.
use crate::workflow::{StoryTestResults, TestCaseResult, TestStatus};
use std::path::Path;

use super::{read_story_content, replace_or_append_section, story_stage, write_story_content};

const TEST_RESULTS_MARKER: &str = "<!-- huskies-test-results:";

/// Write (or overwrite) the `## Test Results` section in a story's content.
///
/// The section contains an HTML comment with JSON for machine parsing and a
/// human-readable summary below it. If the section already exists it is
/// replaced in-place. If the story is not found, this is a no-op.
pub fn write_test_results_to_story_file(
    project_root: &Path,
    story_id: &str,
    results: &StoryTestResults,
) -> Result<(), String> {
    let contents = read_story_content(project_root, story_id)?;

    let json = serde_json::to_string(results)
        .map_err(|e| format!("Failed to serialize test results: {e}"))?;

    let section = build_test_results_section(&json, results);
    let new_contents = replace_or_append_section(&contents, "## Test Results", &section);

    // Write back to content store and CRDT.
    let stage = story_stage(story_id).unwrap_or_else(|| "2_current".to_string());
    write_story_content(project_root, story_id, &stage, &new_contents, None);

    Ok(())
}

/// Read test results from the `## Test Results` section of a story.
///
/// Returns `None` if the story is not found or contains no test results section.
pub fn read_test_results_from_story_file(
    project_root: &Path,
    story_id: &str,
) -> Option<StoryTestResults> {
    let contents = read_story_content(project_root, story_id).ok()?;
    parse_test_results_from_contents(&contents)
}

/// Build the `## Test Results` section text including JSON comment and human-readable summary.
fn build_test_results_section(json: &str, results: &StoryTestResults) -> String {
    let mut s = String::from("## Test Results\n\n");
    s.push_str(&format!("{TEST_RESULTS_MARKER} {json} -->\n\n"));

    // Unit tests
    let (unit_pass, unit_fail) = count_pass_fail(&results.unit);
    s.push_str(&format!(
        "### Unit Tests ({unit_pass} passed, {unit_fail} failed)\n\n"
    ));
    if results.unit.is_empty() {
        s.push_str("*No unit tests recorded.*\n");
    } else {
        for t in &results.unit {
            s.push_str(&format_test_line(t));
        }
    }
    s.push('\n');

    // Integration tests
    let (int_pass, int_fail) = count_pass_fail(&results.integration);
    s.push_str(&format!(
        "### Integration Tests ({int_pass} passed, {int_fail} failed)\n\n"
    ));
    if results.integration.is_empty() {
        s.push_str("*No integration tests recorded.*\n");
    } else {
        for t in &results.integration {
            s.push_str(&format_test_line(t));
        }
    }

    s
}

fn count_pass_fail(tests: &[TestCaseResult]) -> (usize, usize) {
    let pass = tests
        .iter()
        .filter(|t| t.status == TestStatus::Pass)
        .count();
    (pass, tests.len() - pass)
}

fn format_test_line(t: &TestCaseResult) -> String {
    let icon = if t.status == TestStatus::Pass {
        "✅"
    } else {
        "❌"
    };
    match &t.details {
        Some(d) if !d.is_empty() => format!("- {icon} {} — {d}\n", t.name),
        _ => format!("- {icon} {}\n", t.name),
    }
}

/// Parse `StoryTestResults` from the JSON embedded in the `## Test Results` section.
#[allow(clippy::string_slice)] // json_end from rfind("-->") → always a char boundary
fn parse_test_results_from_contents(contents: &str) -> Option<StoryTestResults> {
    for line in contents.lines() {
        let trimmed = line.trim();
        if let Some(rest) = trimmed.strip_prefix(TEST_RESULTS_MARKER) {
            // rest looks like: ` {...} -->`
            if let Some(json_end) = rest.rfind("-->") {
                let json_str = rest[..json_end].trim();
                if let Ok(results) = serde_json::from_str::<StoryTestResults>(json_str) {
                    return Some(results);
                }
            }
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::workflow::{StoryTestResults, TestCaseResult, TestStatus};

    fn make_results() -> StoryTestResults {
        StoryTestResults {
            unit: vec![
                TestCaseResult {
                    name: "unit-pass".to_string(),
                    status: TestStatus::Pass,
                    details: None,
                },
                TestCaseResult {
                    name: "unit-fail".to_string(),
                    status: TestStatus::Fail,
                    details: Some("assertion failed".to_string()),
                },
            ],
            integration: vec![TestCaseResult {
                name: "int-pass".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
        }
    }

    #[test]
    fn write_and_read_test_results_roundtrip() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "8001_story_test",
            "2_current",
            "---\nname: Test\n---\n# Story\n",
            crate::db::ItemMeta::named("Test"),
        );

        let results = make_results();
        write_test_results_to_story_file(tmp.path(), "8001_story_test", &results).unwrap();

        let read_back = read_test_results_from_story_file(tmp.path(), "8001_story_test")
            .expect("should read back results");
        assert_eq!(read_back.unit.len(), 2);
        assert_eq!(read_back.integration.len(), 1);
        assert_eq!(read_back.unit[0].name, "unit-pass");
        assert_eq!(read_back.unit[1].status, TestStatus::Fail);
        assert_eq!(
            read_back.unit[1].details.as_deref(),
            Some("assertion failed")
        );
    }

    #[test]
    fn write_test_results_creates_readable_section() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "8002_story_check",
            "2_current",
            "---\nname: Check\n---\n# Story\n\n## Acceptance Criteria\n\n- [ ] AC1\n",
            crate::db::ItemMeta::named("Check"),
        );

        let results = make_results();
        write_test_results_to_story_file(tmp.path(), "8002_story_check", &results).unwrap();

        let contents = read_story_content(tmp.path(), "8002_story_check").unwrap();
        assert!(contents.contains("## Test Results"));
        assert!(contents.contains("✅ unit-pass"));
        assert!(contents.contains("❌ unit-fail"));
        assert!(contents.contains("assertion failed"));
        assert!(contents.contains("huskies-test-results:"));
        assert!(contents.contains("## Acceptance Criteria"));
    }

    #[test]
    fn write_test_results_overwrites_existing_section() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "8003_story_overwrite",
            "2_current",
            "---\nname: Overwrite\n---\n# Story\n\n## Test Results\n\n<!-- huskies-test-results: {} -->\n\n### Unit Tests (0 passed, 0 failed)\n\n*No unit tests recorded.*\n",
            crate::db::ItemMeta::named("Overwrite"),
        );

        let results = make_results();
        write_test_results_to_story_file(tmp.path(), "8003_story_overwrite", &results).unwrap();

        let contents = read_story_content(tmp.path(), "8003_story_overwrite").unwrap();
        assert!(contents.contains("✅ unit-pass"));
        let count = contents.matches("## Test Results").count();
        assert_eq!(count, 1, "should have exactly one ## Test Results section");
    }

    #[test]
    fn read_test_results_returns_none_when_no_section() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "8004_story_empty",
            "2_current",
            "---\nname: Empty\n---\n# Story\n",
            crate::db::ItemMeta::named("Empty"),
        );

        let result = read_test_results_from_story_file(tmp.path(), "8004_story_empty");
        assert!(result.is_none());
    }

    #[test]
    fn read_test_results_returns_none_for_unknown_story() {
        let tmp = tempfile::tempdir().unwrap();
        let result = read_test_results_from_story_file(tmp.path(), "99_story_unknown");
        assert!(result.is_none());
    }

    #[test]
    fn write_test_results_finds_story_in_any_stage() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        crate::db::write_item_with_content(
            "8005_story_qa",
            "3_qa",
            "---\nname: QA Story\n---\n# Story\n",
            crate::db::ItemMeta::named("QA Story"),
        );

        let results = StoryTestResults {
            unit: vec![TestCaseResult {
                name: "u1".to_string(),
                status: TestStatus::Pass,
                details: None,
            }],
            integration: vec![],
        };
        write_test_results_to_story_file(tmp.path(), "8005_story_qa", &results).unwrap();

        let read_back = read_test_results_from_story_file(tmp.path(), "8005_story_qa").unwrap();
        assert_eq!(read_back.unit.len(), 1);
    }
}
