//! Output Format Tests
//!
//! Tests for JSON report generation using NormalizedReport

use chrono::{DateTime, Utc};
use jsonschema::JSONSchema;
use std::path::PathBuf;

use auditmysite::audit::{normalize, AuditReport, BatchReport};
use auditmysite::cli::WcagLevel;
use auditmysite::output::{format_json_batch, UnifiedReport};
use auditmysite::studio::{StudioAuditResponse, StudioHistoryEntry};
use auditmysite::wcag::{Severity, Violation, WcagResults};

fn create_test_report() -> AuditReport {
    let mut wcag_results = WcagResults::new();

    let violation = Violation::new(
        "1.1.1",
        "Non-text Content",
        WcagLevel::A,
        Severity::High,
        "Image missing alt attribute",
        "node-123",
    )
    .with_role(Some("image".to_string()))
    .with_fix("Add alt attribute to the image")
    .with_help_url("https://www.w3.org/WAI/WCAG21/Understanding/non-text-content");

    wcag_results.violations.push(violation);
    wcag_results.passes = 42;

    let mut report = AuditReport::new(
        "https://example.com".to_string(),
        WcagLevel::AA,
        wcag_results,
        1500,
    );
    report.timestamp = fixed_timestamp();
    report
}

fn fixed_timestamp() -> DateTime<Utc> {
    "2026-01-15T12:00:00Z".parse().unwrap()
}

fn load_schema(name: &str) -> serde_json::Value {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("docs")
        .join(name);
    let text = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("Failed to read schema {}: {}", path.display(), e));
    serde_json::from_str(&text)
        .unwrap_or_else(|e| panic!("Failed to parse schema {}: {}", path.display(), e))
}

fn assert_matches_schema(instance: &serde_json::Value, schema_name: &str) {
    let schema_json = load_schema(schema_name);
    let compiled = JSONSchema::compile(&schema_json)
        .unwrap_or_else(|e| panic!("Failed to compile schema {}: {}", schema_name, e));

    let validation = compiled.validate(instance);
    if let Err(errors) = validation {
        let messages: Vec<String> = errors.map(|err| err.to_string()).collect();
        panic!(
            "JSON did not match schema {}:\n{}",
            schema_name,
            messages.join("\n")
        );
    }
}

#[test]
fn test_json_report_generation() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let unified = UnifiedReport::single(&normalized, &report);

    let json_str = unified.to_json(false).expect("JSON generation failed");

    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("Failed to parse generated JSON");

    assert_eq!(parsed["schema_version"], "2.0");
    assert_eq!(parsed["report_type"], "single");
    assert_eq!(parsed["pages"][0]["url"], "https://example.com");
    assert_eq!(parsed["metadata"]["wcag_level"], "AA");
    assert_eq!(parsed["metadata"]["timestamp"], "2026-01-15T12:00:00Z");
    assert!(
        parsed["summary"]["wcag_coverage"]["automated_criteria"]
            .as_u64()
            .unwrap()
            > 0
    );
    assert!(parsed["summary"]["accessibility_score_breakdown"]
        .as_array()
        .is_some_and(|items| items.len() >= 8));
    assert!(parsed["summary"]["management_risks"]
        .as_array()
        .is_some_and(|items| !items.is_empty()));
    assert!(parsed["summary"]["top_actions"]
        .as_array()
        .is_some_and(|items| !items.is_empty()));
    assert!(parsed["pages"][0]["detail"]["confidence_summary"].is_array());
    assert!(parsed["pages"][0]["detail"]["capabilities"].is_array());
    // Findings are now grouped by rule, with taxonomy fields
    assert!(parsed["pages"][0]["findings"].is_array());
    let findings = parsed["pages"][0]["findings"].as_array().unwrap();
    assert_eq!(findings.len(), 1);
    assert!(findings[0]["dimension"].is_string());
    assert!(findings[0]["subcategory"].is_string());
    assert!(findings[0]["issue_class"].is_string());
    assert_eq!(findings[0]["confidence"], "very_high");
    assert_eq!(findings[0]["false_positive_risk"], "very_low");
    assert_eq!(findings[0]["verification"], "automatically_confirmed");
    assert_eq!(findings[0]["complexity"], "low");
    assert_eq!(findings[0]["bfsg_relevance"], "high");
    assert!(findings[0]["expected_impact"].is_string());
    assert!(findings[0]["remediation_priority"].is_string());

    let fix_guidance = parsed["pages"][0]["detail"]["fix_guidance"]
        .as_array()
        .expect("fix_guidance must be an array");
    assert_eq!(fix_guidance[0]["risk"], "high");
    assert_eq!(fix_guidance[0]["confidence"], "very_high");
    assert_eq!(fix_guidance[0]["complexity"], "low");
    assert_matches_schema(&parsed, "json-report.schema.json");
}

#[test]
fn test_json_report_exposes_confidence_and_capabilities() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let unified = UnifiedReport::single(&normalized, &report);
    let parsed = serde_json::to_value(&unified).expect("serialization must work");

    let detail = &parsed["pages"][0]["detail"];
    let confidence = detail["confidence_summary"]
        .as_array()
        .expect("confidence_summary must be an array");
    let capabilities = detail["capabilities"]
        .as_array()
        .expect("capabilities must be an array");

    assert!(confidence
        .iter()
        .any(|entry| entry["signal"] == "Audit-Vertrauen"));
    assert!(capabilities.iter().any(|entry| {
        entry["signal"] == "WCAG-Regeln & Vorkommen"
            && entry["surfaces"]
                .as_array()
                .is_some_and(|surfaces| surfaces.iter().any(|s| s == "PDF"))
    }));
}

#[test]
fn test_json_report_pretty_print() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let unified = UnifiedReport::single(&normalized, &report);

    let pretty = unified.to_json(true).expect("Pretty JSON failed");
    let compact = unified.to_json(false).expect("Compact JSON failed");

    assert!(pretty.contains('\n'));
    assert!(!compact.contains('\n'));

    let pretty_parsed: serde_json::Value = serde_json::from_str(&pretty).unwrap();
    let compact_parsed: serde_json::Value = serde_json::from_str(&compact).unwrap();
    assert_eq!(pretty_parsed, compact_parsed);
}

#[test]
fn test_report_score_calculation() {
    let clean_report = AuditReport::new(
        "https://example.com".to_string(),
        WcagLevel::AA,
        WcagResults::new(),
        100,
    );
    assert!(clean_report.score >= 90.0);

    let report_with_issues = create_test_report();
    assert!(report_with_issues.score < clean_report.score);
}

#[test]
fn test_batch_json_report_generation() {
    let report = create_test_report();
    let batch = BatchReport::from_reports(vec![report], vec![], 1500);

    let json = format_json_batch(&batch, false).expect("Batch JSON generation failed");
    let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid batch JSON");

    assert_eq!(parsed["metadata"]["timestamp"], "2026-01-15T12:00:00Z");
    assert_eq!(parsed["report_type"], "batch");
    assert_eq!(parsed["summary"]["url_count"], 1);
    assert_eq!(parsed["pages"][0]["url"], "https://example.com");
    assert!(parsed["pages"][0]["findings"].is_array());
    assert_matches_schema(&parsed, "json-batch-report.schema.json");
}

// ─── Studio Contract Tests ────────────────────────────────────────

#[test]
fn test_studio_response_matches_schema() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());

    let json_value = serde_json::to_value(&response).expect("serialization must work");
    assert_matches_schema(&json_value, "studio-contract.schema.json");
}

#[test]
fn test_studio_history_entry_roundtrip() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());
    let entry = StudioHistoryEntry::from_response(&response);

    // Serialize and deserialize must round-trip
    let json = serde_json::to_string(&entry).expect("serialization must work");
    let parsed: StudioHistoryEntry =
        serde_json::from_str(&json).expect("deserialization must work");

    assert_eq!(parsed.url, entry.url);
    assert_eq!(parsed.accessibility_score, entry.accessibility_score);
    assert_eq!(parsed.risk_level, entry.risk_level);
    assert_eq!(parsed.module_scores.len(), entry.module_scores.len());

    // Required fields must be present in JSON
    let obj: serde_json::Value = serde_json::from_str(&json).unwrap();
    let map = obj.as_object().unwrap();
    for field in &[
        "id",
        "url",
        "timestamp",
        "accessibility_score",
        "overall_score",
        "grade",
        "certificate",
        "risk_level",
        "total_issues",
        "critical_issues",
        "high_issues",
        "execution_time_ms",
        "module_scores",
    ] {
        assert!(map.contains_key(*field), "Missing field: {}", field);
    }
}

#[test]
fn test_studio_response_has_all_expected_fields() {
    let report = create_test_report();
    let normalized = normalize(&report);
    let response = StudioAuditResponse::from_normalized(&normalized, &report, "{}".to_string());

    let json_value = serde_json::to_value(&response).expect("serialization must work");
    let obj = json_value.as_object().unwrap();

    // Every required field must be present
    let required = vec![
        "url",
        "timestamp",
        "accessibility_score",
        "overall_score",
        "grade",
        "certificate",
        "risk_level",
        "risk_summary",
        "legal_flags",
        "blocking_issues",
        "critical_issues",
        "high_issues",
        "medium_issues",
        "low_issues",
        "total_issues",
        "module_scores",
        "findings",
        "nodes_analyzed",
        "execution_time_ms",
        "executive_summary",
        "artifacts",
        "json_report",
    ];
    for field in required {
        assert!(obj.contains_key(field), "Missing required field: {}", field);
    }
}
