//! Integration tests: the safety interlock chain (the critical path), alarm
//! auto-raise/clear, command apply-only-on-pass, outage triage, and maintenance.

use mcp_scada::store::ScadaStore;
use mcp_scada::types::*;

fn store() -> ScadaStore {
    ScadaStore::new()
}

fn breaker_control_tag(s: &ScadaStore) -> (String, String) {
    let bkr = s.list_assets(None, Some("breaker"), None).into_iter().next().unwrap();
    let tag = s.list_tags(&bkr.id).into_iter().find(|t| t.point_type == PointType::Control).unwrap();
    (bkr.id, tag.id)
}

#[test]
fn seed_has_assets_across_sectors() {
    let s = store();
    assert_eq!(s.list_sites(None).len(), 4);
    assert!(s.list_assets(None, None, None).len() >= 5);
}

#[test]
fn valid_control_command_executes_and_applies() {
    let s = store();
    let (_bkr, tag) = breaker_control_tag(&s);
    let cmd = s.issue_command(&tag, "open", 0.0, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Executed);
    assert!(cmd.rejection_reason.is_none());
    // point actually changed
    assert_eq!(s.get_tag(&tag).unwrap().value, 0.0);
}

#[test]
fn lockout_blocks_control_and_is_not_applied() {
    let s = store();
    let (bkr, tag) = breaker_control_tag(&s);
    let before = s.get_tag(&tag).unwrap().value;
    s.set_lockout(&bkr, true, "tech").unwrap();
    let cmd = s.issue_command(&tag, "open", 0.0, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Rejected);
    assert!(cmd.rejection_reason.unwrap().contains("locked out"));
    // value unchanged
    assert_eq!(s.get_tag(&tag).unwrap().value, before);
}

#[test]
fn faulted_asset_blocks_control() {
    let s = store();
    let (bkr, tag) = breaker_control_tag(&s);
    s.set_asset_state(&bkr, AssetState::Faulted, "sys").unwrap();
    let cmd = s.issue_command(&tag, "close", 1.0, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Rejected);
    assert!(cmd.rejection_reason.unwrap().contains("faulted"));
}

#[test]
fn critical_alarm_blocks_control() {
    let s = store();
    let (bkr, tag) = breaker_control_tag(&s);
    s.raise_alarm(&bkr, AlarmSeverity::Critical, "overcurrent", "sys").unwrap();
    let cmd = s.issue_command(&tag, "close", 1.0, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Rejected);
    assert!(cmd.rejection_reason.unwrap().contains("critical alarm"));
}

#[test]
fn read_only_point_cannot_be_commanded() {
    let s = store();
    let feeder = s.list_assets(None, Some("feeder"), None).into_iter().next().unwrap();
    let analog = s.list_tags(&feeder.id).into_iter().find(|t| t.point_type == PointType::Analog).unwrap();
    let cmd = s.issue_command(&analog.id, "set", 100.0, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Rejected);
    assert!(cmd.rejection_reason.unwrap().contains("not writable"));
}

#[test]
fn setpoint_out_of_range_rejected() {
    let s = store();
    let reg = s.list_assets(None, Some("regulator"), None).into_iter().next().unwrap();
    let sp = s.list_tags(&reg.id).into_iter().find(|t| t.point_type == PointType::Setpoint).unwrap();
    // range is 20..50
    let high = s.issue_command(&sp.id, "set", 999.0, "operator").unwrap();
    assert_eq!(high.status, CommandStatus::Rejected);
    let ok = s.issue_command(&sp.id, "set", 45.0, "operator").unwrap();
    assert_eq!(ok.status, CommandStatus::Executed);
    assert_eq!(s.get_tag(&sp.id).unwrap().value, 45.0);
}

#[test]
fn binary_control_rejects_non_binary() {
    let s = store();
    let (_b, tag) = breaker_control_tag(&s);
    let cmd = s.issue_command(&tag, "set", 0.5, "operator").unwrap();
    assert_eq!(cmd.status, CommandStatus::Rejected);
    assert!(cmd.rejection_reason.unwrap().contains("0 or 1"));
}

#[test]
fn check_interlocks_is_pure_dry_run() {
    let s = store();
    let (bkr, tag) = breaker_control_tag(&s);
    let before = s.get_tag(&tag).unwrap().value;
    s.set_lockout(&bkr, true, "tech").unwrap();
    let res = s.check_interlocks(&tag, 0.0).unwrap();
    assert_eq!(res["would_execute"], false);
    // dry-run did not change the point and recorded no command
    assert_eq!(s.get_tag(&tag).unwrap().value, before);
    assert_eq!(s.command_history(Some(&bkr), 10).len(), 0);
}

#[test]
fn reading_auto_raises_and_clears_alarm() {
    let s = store();
    let feeder = s.list_assets(None, Some("feeder"), None).into_iter().next().unwrap();
    let load = s.list_tags(&feeder.id).into_iter().find(|t| t.point_type == PointType::Analog).unwrap();
    // crit_high is 580
    s.ingest_reading(&load.id, 590.0, "good", "rtu").unwrap();
    let active = s.list_alarms(true, Some(AlarmSeverity::Critical));
    assert!(active.iter().any(|a| a.tag_id.as_deref() == Some(&load.id)), "critical alarm should be raised");
    // back in band clears it
    s.ingest_reading(&load.id, 400.0, "good", "rtu").unwrap();
    let still = s.list_alarms(true, None);
    assert!(!still.iter().any(|a| a.tag_id.as_deref() == Some(&load.id) && a.state != AlarmState::Cleared));
}

#[test]
fn outage_triage_priority() {
    let s = store();
    let site = s.list_sites(None).into_iter().next().unwrap();
    // safety hazard -> P1 regardless of customers
    let hazard = s.report_outage(&site.id, None, "downed line", 5, true, "dispatch").unwrap();
    assert_eq!(hazard.priority, "P1");
    // big non-hazard -> P1
    let big = s.report_outage(&site.id, None, "feeder trip", 1500, false, "dispatch").unwrap();
    assert_eq!(big.priority, "P1");
    // small -> P4
    let small = s.report_outage(&site.id, None, "single service", 1, false, "dispatch").unwrap();
    assert_eq!(small.priority, "P4");
    // queue is priority-ordered and excludes restored
    s.update_outage_status(&small.id, OutageStatus::Restored, "crew").unwrap();
    let q = s.outage_queue();
    assert!(q.iter().all(|o| o.status != OutageStatus::Restored));
    assert_eq!(q.first().unwrap().priority, "P1");
}

#[test]
fn work_order_lifecycle() {
    let s = store();
    let (bkr, _t) = breaker_control_tag(&s);
    let wo = s.create_work_order(&bkr, "corrective", "high", "Replace trip coil", None, "planner").unwrap();
    assert_eq!(wo.status, WorkOrderStatus::Open);
    let done = s.update_work_order(&wo.id, Some(WorkOrderStatus::Completed), Some("crew-7".into()), None, "planner").unwrap();
    assert_eq!(done.status, WorkOrderStatus::Completed);
    assert!(done.completed_at.is_some());
}
