//! End-to-end verification of the C10 derived position-event table (#114) at the
//! public crate boundary: the World consumes `PositionAction` / `PositionEvent`
//! via the `aura_engine` re-exports and persists the audit table the same way
//! the run registry persists a `RunReport` — through serde_json. These tests pin
//! the *consumer-visible wire shape* (the persisted column form), distinct from
//! the crate-internal type-level unit tests in `report.rs`.

use aura_engine::{PositionAction, PositionEvent, Timestamp};

/// A minimal stop-and-reverse audit table as a broker would derive it: open
/// long, then at one later instant Close-before-open into a short (a reversal,
/// two events sharing `event_ts`). The smallest table that exercises every
/// action variant and the same-ts reversal shape.
fn reversal_table() -> Vec<PositionEvent> {
    let open = Timestamp(1_000);
    let flip = Timestamp(2_000);
    vec![
        PositionEvent {
            event_ts: open,
            action: PositionAction::Buy,
            position_id: 1,
            instrument_id: 3,
            volume: 0.10,
        },
        // reversal at one instant: close the long first, then open the short.
        PositionEvent {
            event_ts: flip,
            action: PositionAction::Close,
            position_id: 1,
            instrument_id: 3,
            volume: 0.10,
        },
        PositionEvent {
            event_ts: flip,
            action: PositionAction::Sell,
            position_id: 2,
            instrument_id: 3,
            volume: 0.10,
        },
    ]
}

/// Property: a derived position-event table survives a full serde_json
/// round-trip through the public re-exports byte-for-byte equal, AND the
/// persisted `action` column is a **bare integer** (C7-scalar / ledger column
/// shape), never a tagged-enum object. The World persists this audit table with
/// the same encoder it uses for `RunReport`; if `action` serialised as
/// `{"Sell":null}` it would break the flat columnar contract and any tool
/// reading the table as `action: i64`. Pins the consumer-visible on-disk shape.
#[test]
fn position_event_table_round_trips_with_bare_int_action_column() {
    let table = reversal_table();
    let json = serde_json::to_string(&table).expect("serialize audit table");

    // action persists as a bare i64 (Buy=0, Sell=1, Close=2), not a tagged enum.
    assert!(json.contains("\"action\":0"), "Buy must encode as action:0 — {json}");
    assert!(json.contains("\"action\":1"), "Sell must encode as action:1 — {json}");
    assert!(json.contains("\"action\":2"), "Close must encode as action:2 — {json}");
    assert!(!json.contains("\"Buy\""), "action must not be a tagged enum — {json}");

    let back: Vec<PositionEvent> = serde_json::from_str(&json).expect("deserialize audit table");
    assert_eq!(back, table, "the audit table must round-trip byte-for-byte (C18 reproducibility)");
}

/// Property: the persisted `PositionAction` integer mapping is the **stable
/// wire encoding** `Buy=0, Sell=1, Close=2`, and any out-of-range integer is
/// **rejected** on read rather than silently coerced. This is the contract every
/// persisted audit table depends on: renumbering the variants (or accepting a
/// stray 3 as some action) would silently mis-read every historical table.
/// Exercised through the public re-export as a downstream reader would.
#[test]
fn position_action_wire_encoding_is_stable_and_validated() {
    // forward: the literal i64 each variant persists as.
    assert_eq!(serde_json::to_string(&PositionAction::Buy).unwrap(), "0");
    assert_eq!(serde_json::to_string(&PositionAction::Sell).unwrap(), "1");
    assert_eq!(serde_json::to_string(&PositionAction::Close).unwrap(), "2");

    // backward: each valid code reads back to its variant.
    assert_eq!(serde_json::from_str::<PositionAction>("0").unwrap(), PositionAction::Buy);
    assert_eq!(serde_json::from_str::<PositionAction>("1").unwrap(), PositionAction::Sell);
    assert_eq!(serde_json::from_str::<PositionAction>("2").unwrap(), PositionAction::Close);

    // an out-of-range code is refused on read, not coerced (the validation arm).
    assert!(serde_json::from_str::<PositionAction>("3").is_err());
    assert!(serde_json::from_str::<PositionAction>("-1").is_err());
}
