//! Property-style tests for public hostile-input boundaries.

use index_capture::{CaptureRequest, capture_redacted};
use index_core::{IndexDocument, IndexNode, IndexUrl, Link};
use index_dom::parse_html;
use index_extract::{ExtractFormat, extract_document, validate_document_json_schema};
use index_renderer::{RenderOptions, render_document};
use index_transformer::{Transformer, state::Empty};

fn transform(html: &str) -> IndexDocument {
    Transformer::<Empty>::new()
        .fetched(html)
        .parse()
        .extract()
        .transform()
        .into_document()
}

fn has_unsafe_control(text: &str) -> bool {
    text.chars()
        .any(|ch| ch != '\n' && ch != '\t' && ch.is_control())
}

#[test]
fn generated_html_fragments_parse_transform_and_extract_json() {
    let samples = [
        "",
        "<html><body><main><h1>ok</h1><p>text",
        "<table><tr><td>a<td>b<tr><td>c</table>",
        "<article><pre><code>cargo test\nRUST_LOG=debug index</code></pre></article>",
        "<main><section><h2>One</h2><ul><li>a<li>b</ul></section></main>",
        "<div><a href=\"javascript:alert(1)\">bad</a><a href=\"/safe\">safe</a></div>",
        "<form action=\"/search\"><input name=\"q\" value=\"knowledge\"></form>",
        "<main>\u{1b}[31mremote control</main>",
    ];

    for sample in samples {
        let _parsed = parse_html(sample);
        let document = transform(sample);
        let json = extract_document(&document, ExtractFormat::Json);
        assert!(
            validate_document_json_schema(&json).is_ok(),
            "document JSON must remain schema-valid for sample: {sample:?}"
        );
    }
}

#[test]
fn renderer_property_strips_remote_terminal_controls() {
    let samples = [
        "plain text",
        "red\u{1b}[31mtext",
        "bell\u{7}text",
        "nul\u{0}text",
        "osc\u{1b}]0;title\u{7}",
        "line\nbreak\tkept",
    ];

    for sample in samples {
        let mut document = IndexDocument::titled(sample);
        document.push(IndexNode::Paragraph(sample.to_owned()));
        document.push(IndexNode::Link(Link::new(
            sample,
            format!("https://example.org/{sample}"),
        )));
        document.push(IndexNode::CodeBlock {
            language: Some("text".to_owned()),
            code: sample.to_owned(),
        });

        let rendered = render_document(&document, RenderOptions::default());
        assert!(
            !has_unsafe_control(&rendered),
            "renderer emitted an unsafe control character for sample: {sample:?}"
        );
    }
}

#[test]
fn unsafe_url_schemes_are_rejected_consistently() {
    let schemes = [
        "javascript",
        "data",
        "vbscript",
        "ftp",
        "mailto",
        "about",
        "chrome",
        "file+ssh",
    ];

    for scheme in schemes {
        let candidate = format!("{scheme}:alert(1)");
        assert!(
            IndexUrl::parse(&candidate).is_err(),
            "unsafe scheme must be rejected: {candidate}"
        );
    }
}

#[test]
fn capture_redaction_removes_generated_credential_values() -> Result<(), Box<dyn std::error::Error>>
{
    let credentials = [
        ("password", "correct-horse-battery-staple"),
        ("token", "tok_live_123456"),
        ("api_key", "key_abcdef"),
        ("session", "session-secret"),
    ];

    for (name, value) in credentials {
        let html = format!(
            "<form><input name=\"{name}\" value=\"{value}\"></form><a href=\"/?{name}={value}\">private</a>"
        );
        let request = CaptureRequest::new("https://example.org/private?token=source-secret", html)?
            .with_diagnostic(format!("{name}={value}; cookie=session=diagnostic-secret"));
        let artifact = capture_redacted(&request);
        let serialized = artifact.to_text();

        assert!(
            !serialized.contains(value),
            "capture artifact retained credential value for key {name}"
        );
        assert!(!serialized.contains("source-secret"));
        assert!(!serialized.contains("diagnostic-secret"));
        assert!(serialized.contains("[REDACTED]"));
        assert!(artifact.validate_bundle().is_ok());
    }
    Ok(())
}
