use compliance_core::models::*;

// ─── Severity ───

#[test]
fn severity_display_all_variants() {
    assert_eq!(Severity::Info.to_string(), "info");
    assert_eq!(Severity::Low.to_string(), "low");
    assert_eq!(Severity::Medium.to_string(), "medium");
    assert_eq!(Severity::High.to_string(), "high");
    assert_eq!(Severity::Critical.to_string(), "critical");
}

#[test]
fn severity_ordering() {
    assert!(Severity::Info < Severity::Low);
    assert!(Severity::Low < Severity::Medium);
    assert!(Severity::Medium < Severity::High);
    assert!(Severity::High < Severity::Critical);
}

#[test]
fn severity_serde_roundtrip() {
    for sev in [
        Severity::Info,
        Severity::Low,
        Severity::Medium,
        Severity::High,
        Severity::Critical,
    ] {
        let json = serde_json::to_string(&sev).unwrap();
        let back: Severity = serde_json::from_str(&json).unwrap();
        assert_eq!(sev, back);
    }
}

#[test]
fn severity_deserialize_lowercase() {
    let s: Severity = serde_json::from_str(r#""critical""#).unwrap();
    assert_eq!(s, Severity::Critical);
}

// ─── FindingStatus ───

#[test]
fn finding_status_display_all_variants() {
    assert_eq!(FindingStatus::Open.to_string(), "open");
    assert_eq!(FindingStatus::Triaged.to_string(), "triaged");
    assert_eq!(FindingStatus::FalsePositive.to_string(), "false_positive");
    assert_eq!(FindingStatus::Resolved.to_string(), "resolved");
    assert_eq!(FindingStatus::Ignored.to_string(), "ignored");
}

#[test]
fn finding_status_serde_roundtrip() {
    for status in [
        FindingStatus::Open,
        FindingStatus::Triaged,
        FindingStatus::FalsePositive,
        FindingStatus::Resolved,
        FindingStatus::Ignored,
    ] {
        let json = serde_json::to_string(&status).unwrap();
        let back: FindingStatus = serde_json::from_str(&json).unwrap();
        assert_eq!(status, back);
    }
}

// ─── Finding ───

#[test]
fn finding_new_defaults() {
    let f = Finding::new(
        "repo1".into(),
        "fp123".into(),
        "semgrep".into(),
        ScanType::Sast,
        "Test title".into(),
        "Test desc".into(),
        Severity::High,
    );
    assert_eq!(f.repo_id, "repo1");
    assert_eq!(f.fingerprint, "fp123");
    assert_eq!(f.scanner, "semgrep");
    assert_eq!(f.scan_type, ScanType::Sast);
    assert_eq!(f.severity, Severity::High);
    assert_eq!(f.status, FindingStatus::Open);
    assert!(f.id.is_none());
    assert!(f.rule_id.is_none());
    assert!(f.confidence.is_none());
    assert!(f.file_path.is_none());
    assert!(f.remediation.is_none());
    assert!(f.suggested_fix.is_none());
    assert!(f.triage_action.is_none());
    assert!(f.developer_feedback.is_none());
}

// ─── ScanType ───

#[test]
fn scan_type_display_all_variants() {
    let cases = vec![
        (ScanType::Sast, "sast"),
        (ScanType::Sbom, "sbom"),
        (ScanType::Cve, "cve"),
        (ScanType::Gdpr, "gdpr"),
        (ScanType::OAuth, "oauth"),
        (ScanType::Graph, "graph"),
        (ScanType::Dast, "dast"),
        (ScanType::SecretDetection, "secret_detection"),
        (ScanType::Lint, "lint"),
        (ScanType::CodeReview, "code_review"),
    ];
    for (variant, expected) in cases {
        assert_eq!(variant.to_string(), expected);
    }
}

#[test]
fn scan_type_serde_roundtrip() {
    for st in [
        ScanType::Sast,
        ScanType::SecretDetection,
        ScanType::CodeReview,
    ] {
        let json = serde_json::to_string(&st).unwrap();
        let back: ScanType = serde_json::from_str(&json).unwrap();
        assert_eq!(st, back);
    }
}

// ─── ScanRun ───

#[test]
fn scan_run_new_defaults() {
    let sr = ScanRun::new("repo1".into(), ScanTrigger::Manual);
    assert_eq!(sr.repo_id, "repo1");
    assert_eq!(sr.trigger, ScanTrigger::Manual);
    assert_eq!(sr.status, ScanRunStatus::Running);
    assert_eq!(sr.current_phase, ScanPhase::ChangeDetection);
    assert!(sr.phases_completed.is_empty());
    assert_eq!(sr.new_findings_count, 0);
    assert!(sr.error_message.is_none());
    assert!(sr.completed_at.is_none());
}

// ─── PentestStatus ───

#[test]
fn pentest_status_display() {
    assert_eq!(pentest::PentestStatus::Running.to_string(), "running");
    assert_eq!(pentest::PentestStatus::Paused.to_string(), "paused");
    assert_eq!(pentest::PentestStatus::Completed.to_string(), "completed");
    assert_eq!(pentest::PentestStatus::Failed.to_string(), "failed");
}

// ─── PentestStrategy ───

#[test]
fn pentest_strategy_display() {
    assert_eq!(pentest::PentestStrategy::Quick.to_string(), "quick");
    assert_eq!(
        pentest::PentestStrategy::Comprehensive.to_string(),
        "comprehensive"
    );
    assert_eq!(pentest::PentestStrategy::Targeted.to_string(), "targeted");
    assert_eq!(
        pentest::PentestStrategy::Aggressive.to_string(),
        "aggressive"
    );
    assert_eq!(pentest::PentestStrategy::Stealth.to_string(), "stealth");
}

// ─── PentestSession ───

#[test]
fn pentest_session_new_defaults() {
    let s = pentest::PentestSession::new("target1".into(), pentest::PentestStrategy::Quick);
    assert_eq!(s.target_id, "target1");
    assert_eq!(s.status, pentest::PentestStatus::Running);
    assert_eq!(s.strategy, pentest::PentestStrategy::Quick);
    assert_eq!(s.tool_invocations, 0);
    assert_eq!(s.tool_successes, 0);
    assert_eq!(s.findings_count, 0);
    assert!(s.completed_at.is_none());
    assert!(s.repo_id.is_none());
}

#[test]
fn pentest_session_success_rate_zero_invocations() {
    let s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Comprehensive);
    assert_eq!(s.success_rate(), 100.0);
}

#[test]
fn pentest_session_success_rate_calculation() {
    let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Comprehensive);
    s.tool_invocations = 10;
    s.tool_successes = 7;
    assert!((s.success_rate() - 70.0).abs() < f64::EPSILON);
}

#[test]
fn pentest_session_success_rate_all_success() {
    let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
    s.tool_invocations = 5;
    s.tool_successes = 5;
    assert_eq!(s.success_rate(), 100.0);
}

#[test]
fn pentest_session_success_rate_none_success() {
    let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
    s.tool_invocations = 3;
    s.tool_successes = 0;
    assert_eq!(s.success_rate(), 0.0);
}

// ─── PentestMessage factories ───

#[test]
fn pentest_message_user() {
    let m = pentest::PentestMessage::user("sess1".into(), "hello".into());
    assert_eq!(m.role, "user");
    assert_eq!(m.session_id, "sess1");
    assert_eq!(m.content, "hello");
    assert!(m.attack_node_id.is_none());
    assert!(m.tool_calls.is_none());
}

#[test]
fn pentest_message_assistant() {
    let m = pentest::PentestMessage::assistant("sess1".into(), "response".into());
    assert_eq!(m.role, "assistant");
}

#[test]
fn pentest_message_tool_result() {
    let m = pentest::PentestMessage::tool_result("sess1".into(), "output".into(), "node1".into());
    assert_eq!(m.role, "tool_result");
    assert_eq!(m.attack_node_id, Some("node1".to_string()));
}

// ─── AttackChainNode ───

#[test]
fn attack_chain_node_new_defaults() {
    let n = pentest::AttackChainNode::new(
        "sess1".into(),
        "node1".into(),
        "recon".into(),
        serde_json::json!({"target": "example.com"}),
        "Starting recon".into(),
    );
    assert_eq!(n.session_id, "sess1");
    assert_eq!(n.node_id, "node1");
    assert_eq!(n.tool_name, "recon");
    assert_eq!(n.status, pentest::AttackNodeStatus::Pending);
    assert!(n.parent_node_ids.is_empty());
    assert!(n.findings_produced.is_empty());
    assert!(n.risk_score.is_none());
    assert!(n.started_at.is_none());
}

// ─── DastTarget ───

#[test]
fn dast_target_new_defaults() {
    let t = dast::DastTarget::new(
        "My App".into(),
        "https://example.com".into(),
        dast::DastTargetType::WebApp,
    );
    assert_eq!(t.name, "My App");
    assert_eq!(t.base_url, "https://example.com");
    assert_eq!(t.target_type, dast::DastTargetType::WebApp);
    assert_eq!(t.max_crawl_depth, 5);
    assert_eq!(t.rate_limit, 10);
    assert!(!t.allow_destructive);
    assert!(t.excluded_paths.is_empty());
    assert!(t.auth_config.is_none());
    assert!(t.repo_id.is_none());
}

#[test]
fn dast_target_type_display() {
    assert_eq!(dast::DastTargetType::WebApp.to_string(), "webapp");
    assert_eq!(dast::DastTargetType::RestApi.to_string(), "rest_api");
    assert_eq!(dast::DastTargetType::GraphQl.to_string(), "graphql");
}

// ─── DastScanRun ───

#[test]
fn dast_scan_run_new_defaults() {
    let sr = dast::DastScanRun::new("target1".into());
    assert_eq!(sr.status, dast::DastScanStatus::Running);
    assert_eq!(sr.current_phase, dast::DastScanPhase::Reconnaissance);
    assert!(sr.phases_completed.is_empty());
    assert_eq!(sr.endpoints_discovered, 0);
    assert_eq!(sr.findings_count, 0);
    assert!(!sr.exploitable_count > 0);
    assert!(sr.completed_at.is_none());
}

#[test]
fn dast_scan_phase_display() {
    assert_eq!(
        dast::DastScanPhase::Reconnaissance.to_string(),
        "reconnaissance"
    );
    assert_eq!(dast::DastScanPhase::Crawling.to_string(), "crawling");
    assert_eq!(dast::DastScanPhase::Completed.to_string(), "completed");
}

// ─── DastVulnType ───

#[test]
fn dast_vuln_type_display_all_variants() {
    let cases = vec![
        (dast::DastVulnType::SqlInjection, "sql_injection"),
        (dast::DastVulnType::Xss, "xss"),
        (dast::DastVulnType::AuthBypass, "auth_bypass"),
        (dast::DastVulnType::Ssrf, "ssrf"),
        (dast::DastVulnType::Idor, "idor"),
        (dast::DastVulnType::Other, "other"),
    ];
    for (variant, expected) in cases {
        assert_eq!(variant.to_string(), expected);
    }
}

// ─── DastFinding ───

#[test]
fn dast_finding_new_defaults() {
    let f = dast::DastFinding::new(
        "run1".into(),
        "target1".into(),
        dast::DastVulnType::Xss,
        "XSS in search".into(),
        "Reflected XSS".into(),
        Severity::High,
        "https://example.com/search".into(),
        "GET".into(),
    );
    assert_eq!(f.vuln_type, dast::DastVulnType::Xss);
    assert_eq!(f.severity, Severity::High);
    assert!(!f.exploitable);
    assert!(f.evidence.is_empty());
    assert!(f.session_id.is_none());
    assert!(f.linked_sast_finding_id.is_none());
}

// ─── SbomEntry ───

#[test]
fn sbom_entry_new_defaults() {
    let e = SbomEntry::new(
        "repo1".into(),
        "lodash".into(),
        "4.17.21".into(),
        "npm".into(),
    );
    assert_eq!(e.name, "lodash");
    assert_eq!(e.version, "4.17.21");
    assert_eq!(e.package_manager, "npm");
    assert!(e.license.is_none());
    assert!(e.purl.is_none());
    assert!(e.known_vulnerabilities.is_empty());
}

// ─── TrackedRepository ───

#[test]
fn tracked_repository_new_defaults() {
    let r = TrackedRepository::new("My Repo".into(), "https://github.com/org/repo.git".into());
    assert_eq!(r.name, "My Repo");
    assert_eq!(r.git_url, "https://github.com/org/repo.git");
    assert_eq!(r.default_branch, "main");
    assert!(!r.webhook_enabled);
    assert!(r.webhook_secret.is_some());
    // Webhook secret should be 32 hex chars (UUID without dashes)
    assert_eq!(r.webhook_secret.as_ref().unwrap().len(), 32);
    assert!(r.tracker_type.is_none());
    assert_eq!(r.findings_count, 0);
}

// ─── ScanTrigger ───

#[test]
fn scan_trigger_serde_roundtrip() {
    for trigger in [
        ScanTrigger::Scheduled,
        ScanTrigger::Webhook,
        ScanTrigger::Manual,
    ] {
        let json = serde_json::to_string(&trigger).unwrap();
        let back: ScanTrigger = serde_json::from_str(&json).unwrap();
        assert_eq!(trigger, back);
    }
}

// ─── PentestEvent serde (tagged enum) ───

#[test]
fn pentest_event_serde_thinking() {
    let event = pentest::PentestEvent::Thinking {
        reasoning: "analyzing target".into(),
    };
    let json = serde_json::to_string(&event).unwrap();
    assert!(json.contains(r#""type":"thinking""#));
    assert!(json.contains("analyzing target"));
}

#[test]
fn pentest_event_serde_finding() {
    let event = pentest::PentestEvent::Finding {
        finding_id: "f1".into(),
        title: "XSS".into(),
        severity: "high".into(),
    };
    let json = serde_json::to_string(&event).unwrap();
    let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
    match back {
        pentest::PentestEvent::Finding {
            finding_id,
            title,
            severity,
        } => {
            assert_eq!(finding_id, "f1");
            assert_eq!(title, "XSS");
            assert_eq!(severity, "high");
        }
        _ => panic!("wrong variant"),
    }
}

// ─── PentestEvent Paused/Resumed ───

#[test]
fn pentest_event_serde_paused() {
    let event = pentest::PentestEvent::Paused;
    let json = serde_json::to_string(&event).unwrap();
    assert!(json.contains(r#""type":"paused""#));
    let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
    assert!(matches!(back, pentest::PentestEvent::Paused));
}

#[test]
fn pentest_event_serde_resumed() {
    let event = pentest::PentestEvent::Resumed;
    let json = serde_json::to_string(&event).unwrap();
    assert!(json.contains(r#""type":"resumed""#));
    let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
    assert!(matches!(back, pentest::PentestEvent::Resumed));
}

// ─── PentestConfig serde ───

#[test]
fn pentest_config_serde_roundtrip() {
    let config = pentest::PentestConfig {
        app_url: "https://example.com".into(),
        git_repo_url: Some("https://github.com/org/repo".into()),
        branch: Some("main".into()),
        commit_hash: None,
        app_type: Some("web".into()),
        rate_limit: Some(10),
        auth: pentest::PentestAuthConfig {
            mode: pentest::AuthMode::Manual,
            username: Some("admin".into()),
            password: Some("pass123".into()),
            registration_url: None,
            verification_email: None,
            imap_host: None,
            imap_port: None,
            imap_username: None,
            imap_password: None,
            cleanup_test_user: true,
        },
        custom_headers: [("X-Token".to_string(), "abc".to_string())]
            .into_iter()
            .collect(),
        strategy: Some("comprehensive".into()),
        allow_destructive: false,
        initial_instructions: Some("Test the login flow".into()),
        scope_exclusions: vec!["/admin".into()],
        disclaimer_accepted: true,
        disclaimer_accepted_at: Some(chrono::Utc::now()),
        environment: pentest::Environment::Staging,
        tester: pentest::TesterInfo {
            name: "Alice".into(),
            email: "alice@example.com".into(),
        },
        max_duration_minutes: Some(30),
        skip_mode: false,
    };
    let json = serde_json::to_string(&config).unwrap();
    let back: pentest::PentestConfig = serde_json::from_str(&json).unwrap();
    assert_eq!(back.app_url, "https://example.com");
    assert_eq!(back.auth.mode, pentest::AuthMode::Manual);
    assert_eq!(back.auth.username, Some("admin".into()));
    assert!(back.auth.cleanup_test_user);
    assert_eq!(back.scope_exclusions, vec!["/admin".to_string()]);
    assert_eq!(back.environment, pentest::Environment::Staging);
}

#[test]
fn pentest_auth_config_default() {
    let auth = pentest::PentestAuthConfig::default();
    assert_eq!(auth.mode, pentest::AuthMode::None);
    assert!(auth.username.is_none());
    assert!(auth.password.is_none());
    assert!(auth.verification_email.is_none());
    assert!(auth.imap_host.is_none());
    assert!(!auth.cleanup_test_user);
}

// ─── TestUserRecord ───

#[test]
fn test_user_record_default() {
    let r = pentest::TestUserRecord::default();
    assert!(r.username.is_none());
    assert!(r.email.is_none());
    assert!(r.provider_user_id.is_none());
    assert!(r.provider.is_none());
    assert!(!r.cleaned_up);
}

#[test]
fn test_user_record_serde_roundtrip() {
    let r = pentest::TestUserRecord {
        username: Some("pentestuser".into()),
        email: Some("pentest+abc@scanner.example.com".into()),
        provider_user_id: Some("kc-uuid-123".into()),
        provider: Some(pentest::IdentityProvider::Keycloak),
        cleaned_up: false,
    };
    let json = serde_json::to_string(&r).unwrap();
    let back: pentest::TestUserRecord = serde_json::from_str(&json).unwrap();
    assert_eq!(back.username, Some("pentestuser".into()));
    assert_eq!(back.provider, Some(pentest::IdentityProvider::Keycloak));
    assert!(!back.cleaned_up);
}

#[test]
fn identity_provider_serde_all_variants() {
    for (variant, expected) in [
        (pentest::IdentityProvider::Keycloak, "\"keycloak\""),
        (pentest::IdentityProvider::Auth0, "\"auth0\""),
        (pentest::IdentityProvider::Okta, "\"okta\""),
        (pentest::IdentityProvider::Firebase, "\"firebase\""),
        (pentest::IdentityProvider::Custom, "\"custom\""),
    ] {
        let json = serde_json::to_string(&variant).unwrap();
        assert_eq!(json, expected);
        let back: pentest::IdentityProvider = serde_json::from_str(&json).unwrap();
        assert_eq!(back, variant);
    }
}

#[test]
fn pentest_session_with_test_user() {
    let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
    assert!(s.test_user.is_none());
    s.test_user = Some(pentest::TestUserRecord {
        username: Some("pentester".into()),
        email: Some("pentest+123@example.com".into()),
        provider_user_id: None,
        provider: Some(pentest::IdentityProvider::Auth0),
        cleaned_up: false,
    });
    let bson_doc = bson::to_document(&s).unwrap();
    let back: pentest::PentestSession = bson::from_document(bson_doc).unwrap();
    assert!(back.test_user.is_some());
    let tu = back.test_user.as_ref().unwrap();
    assert_eq!(tu.username, Some("pentester".into()));
    assert_eq!(tu.provider, Some(pentest::IdentityProvider::Auth0));
}

// ─── Serde helpers (BSON datetime) ───

#[test]
fn bson_datetime_roundtrip_via_finding() {
    let f = Finding::new(
        "repo1".into(),
        "fp".into(),
        "test".into(),
        ScanType::Sast,
        "t".into(),
        "d".into(),
        Severity::Low,
    );
    // Serialize to BSON and back
    let bson_doc = bson::to_document(&f).unwrap();
    let back: Finding = bson::from_document(bson_doc).unwrap();
    // Timestamps should survive (within 1 second tolerance due to ms precision)
    assert!((back.created_at - f.created_at).num_milliseconds().abs() < 1000);
}

#[test]
fn opt_bson_datetime_roundtrip_with_none() {
    let s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
    assert!(s.completed_at.is_none());
    let bson_doc = bson::to_document(&s).unwrap();
    let back: pentest::PentestSession = bson::from_document(bson_doc).unwrap();
    assert!(back.completed_at.is_none());
}

#[test]
fn opt_bson_datetime_roundtrip_with_some() {
    let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
    s.completed_at = Some(chrono::Utc::now());
    let bson_doc = bson::to_document(&s).unwrap();
    let back: pentest::PentestSession = bson::from_document(bson_doc).unwrap();
    assert!(back.completed_at.is_some());
}
