// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
//
// SPDX-License-Identifier: Apache-2.0

//! ES256 (`COSE_Sign1`) interop fixture (br basil-c033).
//!
//! A bare signed `COSE_Sign1` under ES256 (ECDSA P-256 + SHA-256, COSE alg
//! `-7`), built with the `basil-cose` deterministic (RFC 6979) local signer.
//! The vector is checked in as JSON and cross-verified by `veraison/go-cose`
//! (see `crates/basil-tests/tests/cose_go_interop`), proving Basil's ES256
//! `COSE_Sign1` is wire-compatible with the reference implementation.
//!
//! Regenerate with:
//! `BASIL_REGEN_FIXTURES=1 cargo test -p basil-proto --test cose_es256_fixtures`
#![allow(
    clippy::unwrap_used,
    clippy::indexing_slicing,
    clippy::panic,
    clippy::too_many_lines
)]

use std::future::Future;
use std::path::PathBuf;
use std::task::{Context, Poll, Waker};

use basil_cose::{
    ContentType, Es256Signer, ExternalAad, KeyId, P256Verifier, SignParams, VerifySignedParams,
    Zeroizing, build_signed, verify_signed,
};
use serde_json::{Value, json};

/// Poll a ready-immediately future to completion (local key implementations
/// never yield; a `Pending` here is a test bug).
fn block_on<F: Future>(fut: F) -> F::Output {
    let mut cx = Context::from_waker(Waker::noop());
    let mut fut = std::pin::pin!(fut);
    match fut.as_mut().poll(&mut cx) {
        Poll::Ready(v) => v,
        Poll::Pending => panic!("local future pended"),
    }
}

fn hex(bytes: &[u8]) -> String {
    use std::fmt::Write as _;
    bytes.iter().fold(String::new(), |mut out, b| {
        let _ = write!(out, "{b:02x}");
        out
    })
}

fn unhex(s: &str) -> Vec<u8> {
    assert!(s.len() % 2 == 0, "odd hex length");
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
        .collect()
}

// Deterministic test key material (test key only; never a real secret).
const SIGNING_SCALAR: [u8; 32] = [0x1c; 32];
const SIGNING_KID: &str = "client.es256.test";
const CONTENT_TYPE: &str = "application/basil.es256-interop";
const PAYLOAD: &[u8] = b"es256 interop payload";

fn signer() -> Es256Signer {
    Es256Signer::from_secret_bytes(
        KeyId::from_text(SIGNING_KID).unwrap(),
        &Zeroizing::new(SIGNING_SCALAR),
    )
    .unwrap()
}

fn build_sign1() -> Vec<u8> {
    let params = SignParams {
        content_type: ContentType::new(CONTENT_TYPE.to_string()).unwrap(),
        payload: PAYLOAD,
        claims: None,
        external_aad: ExternalAad::empty(),
    };
    block_on(build_signed(&params, &signer()))
        .unwrap()
        .into_vec()
}

fn build_doc() -> Value {
    let s = signer();
    json!({
        "format": "basil-cose-es256-sign1-fixture",
        "version": 1,
        "description": "Bare signed COSE_Sign1 under ES256 (ECDSA P-256 + SHA-256, \
    COSE alg -7), built with basil-cose's deterministic RFC 6979 local signer. Test \
    key only. Consumers must reproduce and verify these bytes byte-for-byte.",
        "key": {
            "algorithm": "es256",
            "key_id": SIGNING_KID,
            "private_scalar_hex": hex(&SIGNING_SCALAR),
            "public_sec1_hex": hex(&s.public_key_sec1()),
        },
        "content_type": CONTENT_TYPE,
        "external_aad_hex": "",
        "payload_hex": hex(PAYLOAD),
        "cose_sign1_hex": hex(&build_sign1()),
    })
}

fn fixture_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/cose-es256-sign1-v1.json")
}

fn checked_in_doc() -> Value {
    let raw = std::fs::read_to_string(fixture_path()).unwrap();
    serde_json::from_str(&raw).unwrap()
}

fn verifier() -> P256Verifier {
    P256Verifier::from_sec1(
        KeyId::from_text(SIGNING_KID).unwrap(),
        &signer().public_key_sec1(),
    )
    .unwrap()
}

#[test]
fn checked_in_es256_fixture_matches_generated_bytes() {
    let doc = build_doc();
    if std::env::var_os("BASIL_REGEN_FIXTURES").is_some() {
        let mut pretty = serde_json::to_string_pretty(&doc).unwrap();
        pretty.push('\n');
        std::fs::write(fixture_path(), pretty).unwrap();
    }
    assert_eq!(
        checked_in_doc(),
        doc,
        "checked-in ES256 fixture does not match generated bytes; regenerate with \
BASIL_REGEN_FIXTURES=1 if the change is intentional"
    );
}

#[test]
fn es256_signature_is_deterministic() {
    // RFC 6979 + low-S: two independent builds are byte-identical.
    assert_eq!(build_sign1(), build_sign1());
}

#[test]
fn es256_fixture_round_trips_through_verify_signed() {
    let doc = checked_in_doc();
    let bytes = unhex(doc["cose_sign1_hex"].as_str().unwrap());
    assert_eq!(
        bytes,
        build_sign1(),
        "rebuild differs from checked-in bytes"
    );
    let verified = block_on(verify_signed(
        &bytes,
        &verifier(),
        &VerifySignedParams {
            external_aad: ExternalAad::empty(),
            validation: None,
        },
    ))
    .unwrap();
    assert_eq!(verified.payload, PAYLOAD);
    assert_eq!(verified.content_type.as_str(), CONTENT_TYPE);
    assert_eq!(
        verified.signer_key_id,
        KeyId::from_text(SIGNING_KID).unwrap()
    );
}

#[test]
fn es256_fixture_rejects_tampered_signature() {
    let mut bytes = build_sign1();
    *bytes.last_mut().unwrap() ^= 0x01;
    let err = block_on(verify_signed(
        &bytes,
        &verifier(),
        &VerifySignedParams {
            external_aad: ExternalAad::empty(),
            validation: None,
        },
    ))
    .unwrap_err();
    assert_eq!(err, basil_cose::VerifyError::SignatureInvalid);
}
