//! Markdown reporter structural validation tests.
//!
//! These tests exercise the contract the markdown reporter commits to:
//!
//! * Summary table at the top with counts (passed / failed / cached /
//!   total) and a pass-rate percentage row.
//! * Per failed test, a `<details><summary>` collapsible block whose
//!   body is a GFM fenced code block holding the diff.
//! * Per passed test, a compact passed-only table (name + duration).
//! * GitHub Flavored Markdown only: pipe tables, triple-backtick
//!   fences, and HTML `<details>`. No `<summary type="...">`, no
//!   tilde fences, no HTML tables.
//! * Hard 65 KB cap on the rendered body. When exceeded we keep the
//!   summary tables and truncate the failure list, ending with a
//!   "...N more failures, view full report at <link>" footer.

use mcptest_core::report::reporters::MarkdownReporter;
use mcptest_core::report::{Report, Reporter, TestFailure, TestResult, TestStatus};

const SIZE_CAP_BYTES: usize = 65_000;

fn render(report: &Report, link: Option<&str>) -> String {
    let mut buf = Vec::new();
    let reporter = match link {
        Some(l) => MarkdownReporter::default().with_full_report_link(l),
        None => MarkdownReporter::default(),
    };
    reporter.render(report, &mut buf).expect("Markdown render");
    String::from_utf8(buf).expect("UTF-8")
}

fn synthetic_many_failures(n: usize, bytes_per_diff: usize) -> Report {
    let mut r = Report::sample();
    r.tests.clear();
    let big = "z".repeat(bytes_per_diff);
    for i in 0..n {
        r.tests.push(TestResult {
            name: format!("failing test {i}"),
            status: TestStatus::Fail,
            duration_ms: 5,
            cache_hit: false,
            started_at: None,
            ended_at: None,
            failure: Some(TestFailure {
                message: format!("assertion {i} failed"),
                diff: Some(big.clone()),
                network_diagnostic: None,
            }),
            file: Some(format!("tests/case_{i}.yml")),
            line: Some(1),
            steps: Vec::new(),
            consensus: None,
            rule_id: None,
            severity: None,
            score: None,
            derived_metrics: Vec::new(),
            natural_language: false,
        });
    }
    r.summary.total = n;
    r.summary.failed = n;
    r.summary.passed = 0;
    r.summary.cache_hits = 0;
    r
}

#[test]
fn rendered_markdown_for_sample_report_carries_required_sections() {
    let body = render(&Report::sample(), None);
    // Summary table at top.
    assert!(body.contains("| Passed | Failed | Cached | Total | Pass rate |"));
    assert!(body.contains("| 2 | 1 | 1 | 3 | 66.7% |"));
    // Failure detail block with diff in fenced code.
    assert!(body.contains("### Failures"));
    assert!(body.contains("<details><summary>"));
    assert!(body.contains("```"));
    assert!(body.contains("expected: error code -32602"));
    // Passed summary table.
    assert!(body.contains("### Passed"));
    assert!(body.contains("| Test | Duration |"));
}

#[test]
fn rendered_markdown_for_sample_full_report_carries_required_sections() {
    let body = render(&Report::sample_full(), None);
    assert!(body.contains("| Passed | Failed | Cached | Total | Pass rate |"));
    // 1 of 4 passed = 25.0%
    assert!(body.contains("| 1 | 3 | 0 | 4 | 25.0% |"));
    assert!(body.contains("### Failures"));
    // Three failed compliance tests, each in its own collapsible.
    let count = body.matches("<details><summary>").count();
    assert_eq!(count, 3);
}

#[test]
fn rendered_markdown_for_all_pass_report_omits_failures_section() {
    let body = render(&Report::sample_all_pass(), None);
    assert!(body.contains("| 1 | 0 | 0 | 1 | 100.0% |"));
    assert!(!body.contains("### Failures"));
    assert!(!body.contains("<details>"));
}

#[test]
fn rendered_markdown_respects_65kb_size_cap_for_200_failures() {
    let r = synthetic_many_failures(200, 1024);
    let body = render(&r, Some("https://example.com/full-report"));
    assert!(
        body.len() <= SIZE_CAP_BYTES,
        "rendered body {} exceeded 65 KB cap",
        body.len()
    );
    // Truncation marker must surface the link and the count of
    // dropped failures.
    assert!(body.contains("more failures"));
    assert!(body.contains("https://example.com/full-report"));
}

#[test]
fn rendered_markdown_uses_gfm_pipe_tables_not_html_tables() {
    let body = render(&Report::sample(), None);
    // Pipe-table header row.
    assert!(body.contains("| Passed | Failed |"));
    // We forbid HTML tables because they render badly on mobile and
    // do not collapse properly inside <details> blocks.
    assert!(!body.contains("<table>"));
    assert!(!body.contains("<tr>"));
    assert!(!body.contains("<td>"));
}

#[test]
fn rendered_markdown_uses_triple_backtick_fences_not_tilde() {
    let body = render(&Report::sample(), None);
    // GFM compatibility: never tilde fences, always triple-backtick.
    assert!(body.contains("```"));
    assert!(!body.contains("~~~"));
}

#[test]
fn rendered_markdown_details_block_holds_diff_inside_fence() {
    let body = render(&Report::sample(), None);
    // The diff body has to sit *inside* a fenced code block, not as
    // raw text inside <details>. Verify by checking the order of
    // markers in the rendered output.
    let summary_pos = body.find("<details><summary>").expect("details opens");
    let fence_pos = body[summary_pos..].find("```").expect("opening code fence");
    let diff_pos = body[summary_pos..]
        .find("expected: error code")
        .expect("diff body present");
    let close_pos = body[summary_pos..]
        .find("</details>")
        .expect("details closes");
    assert!(fence_pos < diff_pos);
    assert!(diff_pos < close_pos);
}

#[test]
fn rendered_markdown_uses_plain_details_summary_not_typed_summary() {
    let body = render(&Report::sample(), None);
    // GFM renders `<details><summary>text</summary>` but does not
    // accept the `type=` attribute some HTML editors emit. Belt and
    // suspenders check so a future cleanup doesn't slip a typed
    // summary tag in by mistake.
    assert!(body.contains("<details><summary>"));
    assert!(!body.contains("<summary type="));
}

#[test]
fn rendered_markdown_keeps_summary_tables_when_truncating() {
    // Even when we truncate the failure list, the top summary table
    // and the pass-rate row must remain so reviewers see the verdict.
    let r = synthetic_many_failures(200, 1024);
    let body = render(&r, Some("https://example.com/full-report"));
    assert!(body.contains("| Passed | Failed | Cached | Total | Pass rate |"));
    let skip_status = body.contains("Run duration:");
    assert!(
        skip_status,
        "expected the run duration line to survive truncation"
    );
}
