//! Test-only fake CCD-shaped callback client (issue #8, extended for
//! payload delivery in #22).
//!
//! Reads a single JSON [`lifeloop::DispatchEnvelope`] document from
//! stdin (carrying the [`lifeloop::CallbackRequest`] and any opaque
//! [`lifeloop::PayloadEnvelope`] bodies the dispatch is delivering with)
//! and writes a single JSON [`lifeloop::CallbackResponse`] to stdout.
//!
//! Behavior is selected by a single positional argument (passed via
//! [`lifeloop::router::SubprocessInvokerConfig::arg`]):
//!
//! * `ok` (default when no arg) — return a valid response. On
//!   `frame.opening` the response echoes the request idempotency_key
//!   and, if the dispatch carried any payloads, copies the first
//!   payload's `body` and `payload_kind` into the response's
//!   `client_payloads` so the integration test can prove the body was
//!   actually delivered through the transport boundary.
//! * `skipped` — return a valid `status=skipped` response;
//! * `malformed` — print non-JSON bytes;
//! * `nonzero` — exit 7 without writing;
//! * `exit_zero_no_read` — exit 0 without reading stdin;
//! * `hang` — sleep far past any reasonable test timeout;
//! * `huge_stdout` — write more bytes than the subprocess transport allows;
//! * `hold_stdout_open` — spawn a descendant that inherits stdout and sleeps.
//!
//! Args are used instead of env vars so each subprocess invocation is
//! independent of process-global state, which matters when cargo runs
//! integration tests in parallel.
//!
//! This binary lives in the lifeloop crate so the integration test can
//! reach it through `env!("CARGO_BIN_EXE_lifeloop-fake-ccd-client")`,
//! which resolves to the build-output path and is portable across
//! Linux and macOS without shell-quoting concerns.

use std::io::{Read, Write};
use std::process::{Command, ExitCode};

use lifeloop::{
    AcceptablePlacement, CallbackResponse, DispatchEnvelope, LifecycleEventKind, PayloadEnvelope,
    PlacementClass, ReceiptStatus, RequirementLevel, SCHEMA_VERSION,
};

const MAX_STDIN_BYTES: u64 = 16 * 1024 * 1024;

fn main() -> ExitCode {
    let behavior = std::env::args().nth(1).unwrap_or_else(|| "ok".into());

    if behavior == "hang" {
        std::thread::sleep(std::time::Duration::from_secs(30));
        return ExitCode::SUCCESS;
    }

    if behavior == "nonzero" {
        // Don't even read stdin; just exit non-zero with a stderr
        // diagnostic the integration test asserts on.
        eprintln!("fake ccd client: simulated transport failure");
        return ExitCode::from(7);
    }

    if behavior == "exit_zero_no_read" {
        return ExitCode::SUCCESS;
    }

    if behavior == "huge_stdout" {
        let chunk = vec![b'x'; 1024 * 1024];
        for _ in 0..17 {
            if std::io::stdout().write_all(&chunk).is_err() {
                return ExitCode::from(4);
            }
        }
        return ExitCode::SUCCESS;
    }

    let mut buf = Vec::new();
    if let Err(e) = std::io::stdin()
        .take(MAX_STDIN_BYTES + 1)
        .read_to_end(&mut buf)
    {
        eprintln!("fake ccd client: stdin read failed: {e}");
        return ExitCode::from(1);
    }
    if buf.len() as u64 > MAX_STDIN_BYTES {
        eprintln!("fake ccd client: stdin exceeds {MAX_STDIN_BYTES} bytes");
        return ExitCode::from(1);
    }

    if behavior == "malformed" {
        // Print bytes that are definitely not a valid CallbackResponse
        // JSON document; the invoker should surface ParseResponse.
        let _ = std::io::stdout().write_all(b"not json at all\n");
        return ExitCode::SUCCESS;
    }

    let envelope: DispatchEnvelope = match serde_json::from_slice(&buf) {
        Ok(e) => e,
        Err(e) => {
            eprintln!("fake ccd client: could not parse dispatch envelope: {e}");
            return ExitCode::from(2);
        }
    };
    let request = envelope.request;
    let payloads = envelope.payloads;

    if behavior == "hold_stdout_open" {
        let _ = Command::new("sh").arg("-c").arg("sleep 30").spawn();
    }

    let response = match (behavior.as_str(), request.event) {
        ("skipped", _) => CallbackResponse::ok(ReceiptStatus::Skipped),
        (_, LifecycleEventKind::FrameOpening) => {
            let mut resp = CallbackResponse::ok(ReceiptStatus::Delivered);
            // If the dispatch envelope delivered payloads, echo the
            // first one's body/kind back in the response so the e2e
            // test can assert that the body was actually transported.
            // Otherwise emit the original synthetic instruction frame
            // so the existing happy-path assertions keep working.
            let (body, kind) = match payloads.first() {
                Some(p) => (
                    p.body
                        .clone()
                        .unwrap_or_else(|| "hello from fake ccd".into()),
                    p.payload_kind.clone(),
                ),
                None => ("hello from fake ccd".into(), "ccd.instruction_frame".into()),
            };
            let body_size = body.len() as u64;
            resp.client_payloads.push(PayloadEnvelope {
                schema_version: SCHEMA_VERSION.to_string(),
                payload_id: "pay-fake-ccd-1".into(),
                client_id: "ccd".into(),
                payload_kind: kind,
                format: "client-defined".into(),
                content_encoding: "utf8".into(),
                body: Some(body),
                body_ref: None,
                byte_size: body_size,
                content_digest: None,
                acceptable_placements: vec![AcceptablePlacement {
                    placement: PlacementClass::PrePromptFrame,
                    requirement: RequirementLevel::Preferred,
                }],
                idempotency_key: request.idempotency_key.clone(),
                expires_at_epoch_s: None,
                redaction: None,
                metadata: serde_json::Map::new(),
            });
            resp
        }
        _ => CallbackResponse::ok(ReceiptStatus::Delivered),
    };

    let bytes = match serde_json::to_vec(&response) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("fake ccd client: could not serialize response: {e}");
            return ExitCode::from(3);
        }
    };
    if let Err(e) = std::io::stdout().write_all(&bytes) {
        eprintln!("fake ccd client: stdout write failed: {e}");
        return ExitCode::from(4);
    }
    ExitCode::SUCCESS
}
