use super::ops::{self, Samples};
use super::*;

fn pic(w: u32, h: u32) -> Samples {
    // Each pixel's bytes are (x, y, 7·x + y), so a position can be read back from a pixel.
    let rgb = (0..h).flat_map(|y| (0..w).flat_map(move |x| [x as u8, y as u8, (7 * x + y) as u8])).collect();
    Samples { w, h, rgb }
}

fn px(s: &Samples, x: u32, y: u32) -> (u8, u8) {
    let i = 3 * (y * s.w + x) as usize;
    (s.rgb[i], s.rgb[i + 1])
}

/// §9's codes are distinct, and `ALL` holds each reason once.
#[test]
fn reasons_are_distinct() {
    let codes: std::collections::BTreeSet<&str> = Reason::ALL.iter().map(|r| r.code()).collect();
    assert_eq!(codes.len(), Reason::ALL.len());
}

/// Every operation of §4.2 but scaling is exact by derived-tracks §3, read from the
/// registry's own composition rule; the explode is pinned by SPEC §7.1.
#[test]
fn exactness_is_the_registrys() {
    for d in Derivation::ALL {
        assert_eq!(d.exact(4), d != Derivation::Scaling, "{}", d.id());
    }
}

/// §4.5: replicate exactly for an exact operation written as a pinned still, or an explode.
#[test]
fn the_check_follows_the_steps() {
    let enc = Encoder { id: "x@1".into(), settings: serde_json::json!({}) };
    assert_eq!(check_of(Derivation::Crop, 1, CAP_IMAGE_PNG, None), Check::Replicate);
    assert_eq!(check_of(Derivation::Crop, 1, CAP_IMAGE_PNG, Some(&enc)), Check::Attest);
    assert_eq!(check_of(Derivation::Crop, 1, CAP_IMAGE_JPEG, Some(&enc)), Check::Attest);
    assert_eq!(check_of(Derivation::Scaling, 1, CAP_IMAGE_PNG, None), Check::Attest);
    assert_eq!(check_of(Derivation::Explode, 1, CAP_IMAGE_JPEG, None), Check::Replicate);
    assert_eq!(check_of(Derivation::GridComposition, 6, CAP_IMAGE_PNG, None), Check::Replicate);
}

/// §4.2's rules, pixel by pixel.
#[test]
fn the_operations_move_samples_as_stated() {
    let s = pic(4, 3);
    let one = |d, p: Params| ops::evaluate(d, &p, std::slice::from_ref(&s)).expect("evaluates");
    let r1 = one(Derivation::Rotation, Params::Rotation { quarter_turns: 1 });
    assert_eq!((r1.w, r1.h), (3, 4));
    // A quarter turn clockwise puts the bottom-left pixel at the top-left.
    assert_eq!(px(&r1, 0, 0), (0, 2));
    assert_eq!(px(&r1, 2, 0), (0, 0));
    let r2 = one(Derivation::Rotation, Params::Rotation { quarter_turns: 2 });
    assert_eq!(px(&r2, 0, 0), (3, 2));
    let r3 = one(Derivation::Rotation, Params::Rotation { quarter_turns: 3 });
    assert_eq!(px(&r3, 0, 0), (3, 0));
    // One turn and three come back to the picture.
    let back = ops::evaluate(Derivation::Rotation, &Params::Rotation { quarter_turns: 3 }, &[r1]).unwrap();
    assert_eq!(back, s);
    let m = one(Derivation::Mirror, Params::Mirror { flip: Flip::LeftRight });
    assert_eq!(px(&m, 0, 1), (3, 1));
    let m = one(Derivation::Mirror, Params::Mirror { flip: Flip::TopBottom });
    assert_eq!(px(&m, 1, 0), (1, 2));
    let c = one(Derivation::Crop, Params::Region { x: 1, y: 1, w: 2, h: 2 });
    assert_eq!((c.w, c.h, px(&c, 0, 0), px(&c, 1, 1)), (2, 2, (1, 1), (2, 2)));
    assert_eq!(one(Derivation::RoiSelection, Params::Region { x: 1, y: 1, w: 2, h: 2 }), c);
    assert!(ops::evaluate(Derivation::Crop, &Params::Region { x: 3, y: 0, w: 2, h: 1 }, std::slice::from_ref(&s)).is_none());
    assert_eq!(one(Derivation::Identity, Params::None), s);
}

#[test]
fn compositions_place_their_inputs() {
    let (a, b) = (pic(2, 2), Samples { w: 2, h: 2, rgb: vec![9; 12] });
    let g = ops::evaluate(Derivation::GridComposition, &Params::Grid { columns: 2, rows: 1 }, &[a.clone(), b.clone()]).unwrap();
    assert_eq!((g.w, g.h), (4, 2));
    assert_eq!(px(&g, 1, 1), (1, 1));
    assert_eq!(px(&g, 2, 0), (9, 9));
    // Inputs of different sizes do not make a grid.
    assert!(ops::evaluate(Derivation::GridComposition, &Params::Grid { columns: 2, rows: 1 }, &[a.clone(), pic(3, 2)]).is_none());
    let o = ops::evaluate(
        Derivation::OverlayComposition,
        &Params::Overlay { width: 3, height: 3, fill: [7, 7, 7], at: vec![(0, 0), (2, 2)] },
        &[a, b],
    )
    .unwrap();
    assert_eq!(px(&o, 2, 0), (7, 7), "the fill where nothing is placed");
    assert_eq!(px(&o, 1, 1), (1, 1));
    assert_eq!(px(&o, 2, 2), (9, 9), "the second input over the canvas, its other pixels dropped");
}

fn record() -> Record {
    let input = Input {
        content_hash: [1; 32],
        capability: CAP_VIDEO_INT_MOTION.into(),
        receipt: [2; 32],
        object_id: 3,
        frame: Some(1),
        samples: Some([4; 32]),
        derivation: None,
        synthesis: None,
    };
    let mut r = edge::draft(Derivation::Crop, Params::Region { x: 0, y: 0, w: 2, h: 2 }, None, CAP_IMAGE_PNG, vec![input], None, "2026-10-20T12:00:00Z");
    r.output = Output {
        content_hash: [5; 32],
        capability: CAP_IMAGE_PNG.into(),
        receipt: [6; 32],
        object_id: 0,
        width: Some(2),
        height: Some(2),
        samples: Some([7; 32]),
    };
    r.signer_id = "did:web:edge.example#k".into();
    r.sign(&ed25519_dalek::SigningKey::from_bytes(&[9; 32]))
}

#[test]
fn a_record_round_trips_and_its_signature_holds() {
    let r = record();
    let text = r.to_json();
    assert_eq!(Record::read(&text).unwrap(), r);
    assert!(r.verify_signature().is_ok());
    assert_eq!(held_hash(&text), Some(r.hash()));
    let mut bad = r.clone();
    bad.output.object_id = 1;
    assert_eq!(bad.verify_signature(), Err(Reason::BadSignature));
}

/// §5.3's rules refuse in their order.
#[test]
fn reading_refuses_by_rule() {
    let r = record();
    let edit = |f: &dyn Fn(&mut serde_json::Value)| {
        let mut v = r.to_value();
        f(&mut v);
        Record::read(&serde_json::to_string(&v).unwrap())
    };
    assert_eq!(Record::read(&r.to_json().replacen("{", "{\"kind\":\"wai-derivation\",", 1)), Err(Reason::RecordMalformed), "a member twice");
    assert_eq!(edit(&|v| v["params"]["x"] = serde_json::json!(true)), Err(Reason::RecordMalformed), "a boolean");
    assert_eq!(edit(&|v| v["issued_at"] = serde_json::json!("yesterday")), Err(Reason::RecordMalformed));
    assert_eq!(edit(&|v| v["derivation"] = serde_json::json!("wai.derive.srgb_fill")), Err(Reason::DerivationUnknown));
    assert_eq!(edit(&|v| v["params"]["w"] = serde_json::json!(0)), Err(Reason::ParamsInvalid));
    assert_eq!(edit(&|v| v["output"]["capability"] = serde_json::json!(CAP_IMAGE_JPEG)), Err(Reason::ParamsInvalid), "a JPEG with no encoder");
    assert_eq!(edit(&|v| v["inputs"][0]["frame"] = serde_json::Value::Null), Err(Reason::InputsInvalid));
    assert_eq!(edit(&|v| v["check"] = serde_json::json!("attest")), Err(Reason::CheckMismatch));
    let energy = serde_json::json!({"joules_micro": 0, "energy_provenance": "ModelBased", "energy_uncertainty": null,
        "energy_counter_filtering": null, "energy_calibration_ref": null});
    assert_eq!(edit(&|v| v["energy"] = energy.clone()), Err(Reason::EnergyInvalid), "a class beside a zero figure");
    let shunt = serde_json::json!({"joules_micro": 9, "energy_provenance": "HwShunt", "energy_uncertainty": null,
        "energy_counter_filtering": null, "energy_calibration_ref": null});
    assert_eq!(edit(&|v| v["energy"] = shunt.clone()), Err(Reason::EnergyInvalid), "the legacy label");
}

/// §4.3's manifest, exactly.
#[test]
fn the_output_manifest_is_pinned() {
    let r = record();
    let m = ops::output_manifest(&r, 2, 2);
    let settings = hex(&ops::settings_digest(&r));
    assert_eq!(
        m,
        format!(
            "{{\"wai\":\"1.0\",\"media\":\"image\",\"intent\":\"replicate\",\"model_requirement\":{{\"capability\":\"wai.image.png\"}},\"conditioning\":{{\"kind\":\"png\"}},\"derived_from\":{{\"motif\":\"{}\",\"motif_capability\":\"wai.video.int_motion\",\"renderer\":\"wai.derive.crop@1\",\"settings\":\"{settings}\"}},\"target\":{{\"frames\":1,\"h\":2,\"w\":2}}}}",
            hex(&[1; 32])
        )
    );
    // It reads as a WAI1 manifest.
    let env = ops::wai1(m.as_bytes(), b"x");
    assert_eq!(ops::wai1_capability(&env).as_deref(), Some(CAP_IMAGE_PNG));
}

/// §5.3 rule 1 holds the provenance items to their order, and rule 7 the label to its inputs'.
#[test]
fn provenance_and_label_are_read_by_rule() {
    let mut r = record();
    let item = |d: u8, c| ProvItem { input: 0, kind: ProvKind::C2pa, digest: [d; 32], compartment: c };
    r.provenance = vec![item(1, Compartment::System), item(2, Compartment::Personal)];
    let k = ed25519_dalek::SigningKey::from_bytes(&[9; 32]);
    let ok = r.clone().sign(&k);
    assert_eq!(Record::read(&ok.to_json()).unwrap(), ok);
    let mut out_of_order = r.clone();
    out_of_order.provenance.reverse();
    assert_eq!(Record::read(&out_of_order.sign(&k).to_json()), Err(Reason::RecordMalformed));
    let mut past = r.clone();
    past.provenance[1].input = 1;
    assert_eq!(Record::read(&past.sign(&k).to_json()), Err(Reason::RecordMalformed), "an item of an input the record does not have");
    let mut lowered = r.clone();
    lowered.inputs[0].synthesis = Some(Synthesis::Generate);
    lowered.synthesis = Some(Synthesis::Assist);
    assert_eq!(Record::read(&lowered.clone().sign(&k).to_json()), Err(Reason::SynthesisDropped));
    lowered.synthesis = Some(Synthesis::Generate);
    assert!(Record::read(&lowered.sign(&k).to_json()).is_ok(), "a label may rise");
}

#[test]
fn embedded_stores_and_claims_are_found() {
    let s = Samples { w: 2, h: 1, rgb: vec![1, 2, 3, 4, 5, 6] };
    let mut png = crate::png_still::encode(&s.rgb, 2, 1);
    let rest = png.split_off(33);
    crate::png_still::chunk(&mut png, &C2PA_PNG_CHUNK, b"store");
    png.extend_from_slice(&rest);
    let env = ops::wai1(b"{\"wai\":\"1.0\",\"media\":\"image\",\"intent\":\"replicate\",\"model_requirement\":{\"capability\":\"wai.image.png\"},\"conditioning\":{\"kind\":\"png\"}}", &png);
    assert_eq!(ops::embedded_c2pa(CAP_IMAGE_PNG, &env), vec![b"store".to_vec()]);
    assert_eq!(ops::read_samples(CAP_IMAGE_PNG, &env, 0), Some(s), "a store does not stop the reading");
    let claim = r#"{"kind":"claim","subject_type":"object","subject":"0101010101010101010101010101010101010101010101010101010101010101","class":"x"}"#;
    assert_eq!(claim_about(claim).map(|c| c.0), Some([1; 32]));
    assert!(claim_about(&claim.replace("\"object\"", "\"receipt\"")).is_none());
}
