//! Credential-safe native port of the `VivoxTest` validation program.

use clap::Parser;
use libremetaverse::types::compat::{CancellationToken, Uri};
use libremetaverse::{GridClient, NetworkManager, Simulator};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_voice_vivox::{VivoxControlClient, VivoxError};
use roxmltree::Document;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::net::{Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;

pub const EXIT_SUCCESS: u8 = 0;
pub const EXIT_USAGE: u8 = 2;
pub const EXIT_INPUT: u8 = 3;
pub const EXIT_SERVICE: u8 = 4;

const MAX_SCRIPT_BYTES: u64 = 1024 * 1024;
const MAX_SCRIPT_LINES: usize = 256;
const MAX_FIELD_BYTES: usize = 16 * 1024;
const MAX_CONTROL_FRAME_BYTES: usize = 1024 * 1024;
const LIVE_CONFIRMATION: &str = "LOGIN";

#[derive(Parser)]
#[command(
    name = "vivox-test",
    version,
    about = "Validate native LibreMetaverse Vivox control and grid voice flows",
    long_about = "Connects to an already-running proprietary Vivox SDK control service. MetaCrate does not bundle, locate, start, or invoke the Vivox daemon or the C# implementation.",
    arg_required_else_help = true,
    after_help = "Offline script directives (tab-separated): capture-device, current-capture, render-device, current-render, provision, parcel, participant, and reject. Live grid login requires --allow-live-login --confirm-live-login LOGIN. Joining parcel audio additionally requires --allow-session-audio. Prefer GRID_PASSWORD over a positional password so it is not exposed in process listings."
)]
struct Cli {
    /// Avatar first name. May also be supplied as `GRID_FIRST_NAME`.
    #[arg(value_name = "FIRSTNAME")]
    first_name: Option<String>,
    /// Avatar last name. May also be supplied as `GRID_LAST_NAME`.
    #[arg(value_name = "LASTNAME")]
    last_name: Option<String>,
    /// Avatar password. Prefer `GRID_PASSWORD` to avoid process-list exposure.
    #[arg(value_name = "PASSWORD")]
    password: Option<String>,

    /// Run the complete connector/account/session/participant flow against an in-process fake TCP service.
    #[arg(long, value_name = "FILE")]
    fake_script: Option<PathBuf>,
    /// Address of an already-running Vivox SDK control service.
    #[arg(long, default_value = "127.0.0.1:44124", value_name = "IP:PORT")]
    daemon_endpoint: SocketAddr,
    /// Override the grid login endpoint; `GRID_LOGIN_URL` is the fallback.
    #[arg(long, value_name = "URL")]
    login_uri: Option<String>,
    /// Vivox account-management host used to create the connector.
    #[arg(long, default_value = "bhr.vivox.com", value_name = "HOST")]
    voice_server: String,
    /// Maximum time for each daemon, grid, or capability operation.
    #[arg(long, default_value_t = 45, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=300))]
    timeout_seconds: u64,
    /// Explicitly permit a live grid login.
    #[arg(long)]
    allow_live_login: bool,
    /// Required literal confirmation for live login.
    #[arg(long, value_name = "LOGIN")]
    confirm_live_login: Option<String>,
    /// Join the parcel voice session and exercise participant controls in live mode.
    #[arg(long, requires = "allow_live_login")]
    allow_session_audio: bool,
}

#[derive(Debug)]
enum ProgramError {
    Usage(&'static str),
    Input {
        line: usize,
        reason: &'static str,
    },
    Io {
        action: &'static str,
        source: io::Error,
    },
    Voice(VivoxError),
    Grid(&'static str),
    Timeout(&'static str),
    FakeServer(&'static str),
}

impl ProgramError {
    const fn exit_code(&self) -> u8 {
        match self {
            Self::Usage(_) => EXIT_USAGE,
            Self::Input { .. } | Self::Io { .. } => EXIT_INPUT,
            Self::Voice(_) | Self::Grid(_) | Self::Timeout(_) | Self::FakeServer(_) => EXIT_SERVICE,
        }
    }
}

impl fmt::Display for ProgramError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Usage(message) => formatter.write_str(message),
            Self::Input { line, reason } => {
                write!(
                    formatter,
                    "invalid Vivox fake script at line {line}: {reason}"
                )
            }
            Self::Io { action, source } => write!(formatter, "{action}: {source}"),
            Self::Voice(error) => error.fmt(formatter),
            Self::Grid(message) => write!(formatter, "grid voice validation failed: {message}"),
            Self::Timeout(action) => write!(formatter, "{action} timed out"),
            Self::FakeServer(message) => write!(formatter, "fake Vivox service failed: {message}"),
        }
    }
}

impl From<VivoxError> for ProgramError {
    fn from(error: VivoxError) -> Self {
        Self::Voice(error)
    }
}

#[derive(Clone)]
struct Provisioning {
    account_name: String,
    password: String,
    voice_server: String,
}

#[derive(Clone)]
struct ParcelVoice {
    region_name: String,
    local_id: i32,
    channel_uri: String,
}

#[derive(Clone)]
struct FakeParticipant {
    account_name: String,
    display_name: String,
    uri: String,
}

#[derive(Clone)]
struct Rejection {
    return_code: i32,
    status_code: i32,
    status: String,
}

#[derive(Clone)]
struct FakeConfig {
    capture_devices: Vec<String>,
    current_capture: Option<String>,
    render_devices: Vec<String>,
    current_render: Option<String>,
    provision: Provisioning,
    parcel: ParcelVoice,
    participant: FakeParticipant,
    rejections: HashMap<String, Rejection>,
}

struct LiveConfig {
    first_name: String,
    last_name: String,
    password: String,
    login_uri: Option<String>,
    voice_server: String,
    daemon_endpoint: SocketAddr,
    timeout: Duration,
    exercise_session: bool,
}

/// Runs the `VivoxTest` command.
#[must_use]
pub fn main_entry() -> ExitCode {
    let cli = Cli::parse();
    let Ok(runtime) = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
    else {
        eprintln!("vivox-test: could not initialize the async runtime");
        return ExitCode::from(EXIT_SERVICE);
    };
    match runtime.block_on(run(cli)) {
        Ok(()) => ExitCode::from(EXIT_SUCCESS),
        Err(error) => {
            eprintln!("vivox-test: {error}");
            ExitCode::from(error.exit_code())
        }
    }
}

async fn run(cli: Cli) -> Result<(), ProgramError> {
    if let Some(script) = cli.fake_script {
        if cli.first_name.is_some()
            || cli.last_name.is_some()
            || cli.password.is_some()
            || cli.allow_live_login
            || cli.confirm_live_login.is_some()
            || cli.allow_session_audio
        {
            return Err(ProgramError::Usage(
                "credentials and live opt-ins cannot be combined with --fake-script",
            ));
        }
        let config = read_fake_script(&script)?;
        return run_fake(config, Duration::from_secs(cli.timeout_seconds)).await;
    }
    let config = resolve_live(cli)?;
    run_live(config).await
}

fn resolve_live(cli: Cli) -> Result<LiveConfig, ProgramError> {
    if !cli.allow_live_login || cli.confirm_live_login.as_deref() != Some(LIVE_CONFIRMATION) {
        return Err(ProgramError::Usage(
            "live mode requires --allow-live-login --confirm-live-login LOGIN",
        ));
    }
    let first_name = required(cli.first_name, "GRID_FIRST_NAME", "FIRSTNAME is required")?;
    let last_name = required(cli.last_name, "GRID_LAST_NAME", "LASTNAME is required")?;
    let password = required(cli.password, "GRID_PASSWORD", "PASSWORD is required")?;
    validate_field(&cli.voice_server, 0)?;
    if cli.voice_server.contains('/') || cli.voice_server.chars().any(char::is_whitespace) {
        return Err(ProgramError::Usage("--voice-server must be a host name"));
    }
    Ok(LiveConfig {
        first_name,
        last_name,
        password,
        login_uri: cli
            .login_uri
            .or_else(|| std::env::var("GRID_LOGIN_URL").ok())
            .filter(|value| !value.is_empty()),
        voice_server: cli.voice_server,
        daemon_endpoint: cli.daemon_endpoint,
        timeout: Duration::from_secs(cli.timeout_seconds),
        exercise_session: cli.allow_session_audio,
    })
}

fn required(
    value: Option<String>,
    environment: &str,
    message: &'static str,
) -> Result<String, ProgramError> {
    value
        .or_else(|| std::env::var(environment).ok())
        .filter(|value| !value.is_empty())
        .ok_or(ProgramError::Usage(message))
}

async fn run_fake(config: FakeConfig, operation_timeout: Duration) -> Result<(), ProgramError> {
    let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
        .await
        .map_err(|source| ProgramError::Io {
            action: "binding fake Vivox service",
            source,
        })?;
    let endpoint = listener.local_addr().map_err(|source| ProgramError::Io {
        action: "reading fake Vivox endpoint",
        source,
    })?;
    let server_config = config.clone();
    let server = tokio::spawn(async move { serve_fake(listener, server_config).await });
    let mut client = VivoxControlClient::connect(endpoint, operation_timeout).await?;
    let workflow =
        exercise_control_flow(&mut client, &config.provision, &config.parcel, true, true).await;
    let teardown = client.shutdown().await;
    let server_actions = tokio::time::timeout(operation_timeout, server)
        .await
        .map_err(|_| ProgramError::Timeout("fake Vivox service teardown"))?
        .map_err(|_| ProgramError::FakeServer("service task panicked"))??;
    workflow?;
    teardown?;
    let expected = [
        "Aux.GetCaptureDevices.1",
        "Aux.GetRenderDevices.1",
        "Connector.Create.1",
        "Account.Login.1",
        "Session.Create.1",
        "Session.Connect.1",
        "Session.SetParticipantVolumeForMe.1",
        "Session.Terminate.1",
        "Account.Logout.1",
        "Connector.InitiateShutdown.1",
    ];
    if server_actions != expected {
        return Err(ProgramError::FakeServer(
            "control action sequence was incomplete or out of order",
        ));
    }
    println!(
        "Fake Vivox validation complete; requests={} active-pipes=0 active-sessions=0 active-tasks=0",
        server_actions.len()
    );
    Ok(())
}

async fn run_live(mut config: LiveConfig) -> Result<(), ProgramError> {
    println!(
        "Connecting to the configured Vivox SDK control service (the proprietary daemon is not bundled or started)..."
    );
    let mut voice = VivoxControlClient::connect(config.daemon_endpoint, config.timeout).await?;
    print_devices(&mut voice).await?;

    let client = GridClient::new().map_err(|_| ProgramError::Grid("construct GridClient"))?;
    let network = client.network();
    let mut login = network
        .default_login_params(
            std::mem::take(&mut config.first_name),
            std::mem::take(&mut config.last_name),
            std::mem::take(&mut config.password),
            "VivoxTest".into(),
            env!("CARGO_PKG_VERSION").into(),
        )
        .map_err(|_| ProgramError::Grid("build login parameters"))?;
    if let Some(uri) = config.login_uri.take() {
        login.uri = uri;
    }
    println!("Logging into the grid with redacted credentials...");
    let login_result = tokio::time::timeout(
        config.timeout,
        network.login_with_login_params_cancellation_token(login, None),
    )
    .await;
    let logged_in = match login_result {
        Ok(Ok(value)) => value,
        Ok(Err(_)) => false,
        Err(_) => {
            let _ = network.abort_login();
            let _ = voice.shutdown().await;
            return Err(ProgramError::Timeout("grid login"));
        }
    };
    if !logged_in {
        let _ = voice.shutdown().await;
        return Err(ProgramError::Grid("login was rejected"));
    }
    println!("Grid login succeeded; login endpoint and session credentials are redacted.");

    let operation = async {
        wait_for_event_queue(&network, config.timeout).await?;
        let provision =
            request_provisioning(&client, &network, config.timeout, &config.voice_server).await?;
        let parcel = request_parcel_voice(&client, &network, config.timeout).await?;
        if provision.voice_server != config.voice_server {
            println!(
                "Provisioned voice host differs from the configured host; both values are redacted."
            );
        }
        exercise_control_flow(
            &mut voice,
            &provision,
            &parcel,
            config.exercise_session,
            false,
        )
        .await
    }
    .await;
    let voice_teardown = voice.shutdown().await;
    let _ = network.logout_with_method();
    operation?;
    voice_teardown?;
    println!("Live Vivox validation complete; grid, account, sessions, and control pipe closed.");
    Ok(())
}

async fn exercise_control_flow(
    client: &mut VivoxControlClient,
    provision: &Provisioning,
    parcel: &ParcelVoice,
    exercise_session: bool,
    enumerate_devices: bool,
) -> Result<(), ProgramError> {
    if enumerate_devices {
        print_devices(client).await?;
    }
    let management_server = format!("https://www.{}/api2/", provision.voice_server);
    client.create_connector(&management_server).await?;
    println!("Voice connector created: <redacted>");
    client
        .login(&provision.account_name, &provision.password)
        .await?;
    println!("Provisioned voice account logged in: <redacted>");
    println!(
        "Parcel voice info: region={} local-id={} channel=<redacted-uri>",
        parcel.region_name, parcel.local_id
    );
    if exercise_session {
        let session = client
            .create_session(&parcel.channel_uri, &parcel.region_name, None)
            .await?;
        client.connect_session(&session).await?;
        println!("Voice session connected: <redacted>");
        let mut participant_event = None;
        for _ in 0..32 {
            let event = client.next_event().await?;
            if event.kind == "ParticipantStateChangeEvent"
                || event.kind == "ParticipantPropertiesEvent"
            {
                participant_event = Some(event);
                break;
            }
        }
        let event = participant_event.ok_or(ProgramError::Grid(
            "participant event was not observed within 32 daemon events",
        ))?;
        let participant = event.participant_uri.ok_or(ProgramError::FakeServer(
            "participant event omitted its URI",
        ))?;
        client
            .set_participant_volume(&session, &participant, 0)
            .await?;
        println!(
            "Participant control validated for {} (URI redacted).",
            event
                .display_name
                .as_deref()
                .unwrap_or("unnamed participant")
        );
        client.terminate_session(&session).await?;
        println!("Voice session terminated.");
    } else {
        println!(
            "Session audio not joined; pass --allow-session-audio for live participant validation."
        );
    }
    client.logout().await?;
    println!("Voice account logged out.");
    Ok(())
}

async fn print_devices(client: &mut VivoxControlClient) -> Result<(), ProgramError> {
    let capture = client.capture_devices().await?;
    println!("Capture devices (current marked '*'):");
    for device in capture.available {
        let marker = if capture.current.as_deref() == Some(device.as_str()) {
            '*'
        } else {
            ' '
        };
        println!("  {marker} {device}");
    }
    let render = client.render_devices().await?;
    println!("Render devices (current marked '*'):");
    for device in render.available {
        let marker = if render.current.as_deref() == Some(device.as_str()) {
            '*'
        } else {
            ' '
        };
        println!("  {marker} {device}");
    }
    Ok(())
}

async fn wait_for_event_queue(
    network: &NetworkManager,
    operation_timeout: Duration,
) -> Result<Simulator, ProgramError> {
    tokio::time::timeout(operation_timeout, async {
        loop {
            if let Some(simulator) = network.current_sim()
                && simulator
                    .is_event_queue_running(Some(false))
                    .unwrap_or(false)
            {
                return simulator;
            }
            tokio::time::sleep(Duration::from_millis(100)).await;
        }
    })
    .await
    .map_err(|_| ProgramError::Timeout("EventQueueRunning"))
}

async fn request_provisioning(
    client: &GridClient,
    network: &NetworkManager,
    operation_timeout: Duration,
    fallback_voice_server: &str,
) -> Result<Provisioning, ProgramError> {
    let simulator = wait_for_event_queue(network, operation_timeout).await?;
    let response = post_empty_capability(
        client,
        &simulator,
        "ProvisionVoiceAccountRequest",
        operation_timeout,
    )
    .await?;
    let OSD::Map(map) = response else {
        return Err(ProgramError::Grid("provision response was not an LLSD map"));
    };
    let account_name = osd_string(&map, "username")?;
    let password = osd_string(&map, "password")?;
    let voice_server = map
        .get("voice_server")
        .and_then(|value| value.as_string().ok())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| fallback_voice_server.to_owned());
    println!("Provisional voice account created: username/password redacted.");
    Ok(Provisioning {
        account_name,
        password,
        voice_server,
    })
}

async fn request_parcel_voice(
    client: &GridClient,
    network: &NetworkManager,
    operation_timeout: Duration,
) -> Result<ParcelVoice, ProgramError> {
    let simulator = wait_for_event_queue(network, operation_timeout).await?;
    let response = post_empty_capability(
        client,
        &simulator,
        "ParcelVoiceInfoRequest",
        operation_timeout,
    )
    .await?;
    let OSD::Map(map) = response else {
        return Err(ProgramError::Grid(
            "parcel voice response was not an LLSD map",
        ));
    };
    let region_name = osd_string(&map, "region_name")?;
    let local_id = map
        .get("parcel_local_id")
        .and_then(|value| value.as_integer().ok())
        .ok_or(ProgramError::Grid("parcel response omitted local ID"))?;
    let channel_uri = match map.get("voice_credentials") {
        Some(OSD::Map(credentials)) => osd_string(credentials, "channel_uri")?,
        _ => {
            return Err(ProgramError::Grid(
                "parcel response omitted voice credentials",
            ));
        }
    };
    Ok(ParcelVoice {
        region_name,
        local_id,
        channel_uri,
    })
}

async fn post_empty_capability(
    client: &GridClient,
    simulator: &Simulator,
    capability: &'static str,
    operation_timeout: Duration,
) -> Result<OSD, ProgramError> {
    let uri = simulator
        .native_capability_uri(capability)
        .map_err(|_| ProgramError::Grid("capability lookup failed"))?
        .ok_or(ProgramError::Grid("required voice capability is missing"))?;
    let http = client.http_caps_client();
    let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
        Uri(uri.0),
        OSDFormat::Xml,
        OSD::Map(HashMap::new()),
        CancellationToken::default(),
        None,
    );
    let (response, bytes) = tokio::time::timeout(operation_timeout, request)
        .await
        .map_err(|_| ProgramError::Timeout(capability))?
        .map_err(|_| ProgramError::Grid("voice capability request failed"))?;
    if !response.is_success_status_code()
        || u64::try_from(bytes.len()).unwrap_or(u64::MAX) > MAX_SCRIPT_BYTES
    {
        return Err(ProgramError::Grid("voice capability returned an error"));
    }
    OSDParser::deserialize_with_bytes(bytes)
        .map_err(|_| ProgramError::Grid("voice capability returned malformed LLSD"))
}

fn osd_string(map: &HashMap<String, OSD>, key: &'static str) -> Result<String, ProgramError> {
    map.get(key)
        .and_then(|value| value.as_string().ok())
        .filter(|value| !value.is_empty())
        .ok_or(ProgramError::Grid(
            "voice capability response omitted a required string",
        ))
}

#[derive(Default)]
struct FakeConfigBuilder {
    capture_devices: Vec<String>,
    current_capture: Option<String>,
    render_devices: Vec<String>,
    current_render: Option<String>,
    provision: Option<Provisioning>,
    parcel: Option<ParcelVoice>,
    participant: Option<FakeParticipant>,
    rejections: HashMap<String, Rejection>,
}

fn read_fake_script(path: &Path) -> Result<FakeConfig, ProgramError> {
    let file = File::open(path).map_err(|source| ProgramError::Io {
        action: "opening Vivox fake script",
        source,
    })?;
    if file
        .metadata()
        .map_err(|source| ProgramError::Io {
            action: "reading Vivox fake script metadata",
            source,
        })?
        .len()
        > MAX_SCRIPT_BYTES
    {
        return Err(ProgramError::Input {
            line: 0,
            reason: "script exceeds 1 MiB",
        });
    }
    let mut text = String::new();
    file.take(MAX_SCRIPT_BYTES + 1)
        .read_to_string(&mut text)
        .map_err(|source| ProgramError::Io {
            action: "reading Vivox fake script as UTF-8",
            source,
        })?;
    if text.len() as u64 > MAX_SCRIPT_BYTES {
        return Err(ProgramError::Input {
            line: 0,
            reason: "script exceeds 1 MiB",
        });
    }

    let mut builder = FakeConfigBuilder::default();
    let mut directives = 0;
    for (index, raw) in text.lines().enumerate() {
        let line = index + 1;
        let raw = raw.trim_end_matches('\r');
        if raw.trim().is_empty() || raw.trim_start().starts_with('#') {
            continue;
        }
        directives += 1;
        if directives > MAX_SCRIPT_LINES {
            return Err(ProgramError::Input {
                line,
                reason: "script contains more than 256 directives",
            });
        }
        let fields: Vec<_> = raw.split('\t').collect();
        for field in &fields {
            validate_field(field, line)?;
        }
        builder.parse_directive(&fields, line)?;
    }
    builder.finish()
}

impl FakeConfigBuilder {
    fn parse_directive(&mut self, fields: &[&str], line: usize) -> Result<(), ProgramError> {
        match fields {
            ["capture-device", name] => self.capture_devices.push((*name).into()),
            ["current-capture", name] if self.current_capture.is_none() => {
                self.current_capture = Some((*name).into());
            }
            ["render-device", name] => self.render_devices.push((*name).into()),
            ["current-render", name] if self.current_render.is_none() => {
                self.current_render = Some((*name).into());
            }
            ["provision", account_name, password, voice_server] if self.provision.is_none() => {
                self.provision = Some(Provisioning {
                    account_name: (*account_name).into(),
                    password: (*password).into(),
                    voice_server: (*voice_server).into(),
                });
            }
            ["parcel", region_name, local_id, channel_uri] if self.parcel.is_none() => {
                self.parcel = Some(ParcelVoice {
                    region_name: (*region_name).into(),
                    local_id: parse_script_i32(local_id, line, "parcel local ID")?,
                    channel_uri: (*channel_uri).into(),
                });
            }
            ["participant", account_name, display_name, uri] if self.participant.is_none() => {
                self.participant = Some(FakeParticipant {
                    account_name: (*account_name).into(),
                    display_name: (*display_name).into(),
                    uri: (*uri).into(),
                });
            }
            ["reject", action, return_code, status_code, status] => {
                let rejection = Rejection {
                    return_code: parse_script_i32(return_code, line, "reject return code")?,
                    status_code: parse_script_i32(status_code, line, "reject status code")?,
                    status: (*status).into(),
                };
                if self
                    .rejections
                    .insert((*action).into(), rejection)
                    .is_some()
                {
                    return Err(script_error(line, "reject action is duplicated"));
                }
            }
            _ => {
                return Err(script_error(
                    line,
                    "unknown, duplicated, or malformed directive",
                ));
            }
        }
        Ok(())
    }

    fn finish(self) -> Result<FakeConfig, ProgramError> {
        let provision = self
            .provision
            .ok_or_else(|| script_error(0, "one provision directive is required"))?;
        let parcel = self
            .parcel
            .ok_or_else(|| script_error(0, "one parcel directive is required"))?;
        let participant = self
            .participant
            .ok_or_else(|| script_error(0, "one participant directive is required"))?;
        if self.capture_devices.is_empty() || self.render_devices.is_empty() {
            return Err(script_error(
                0,
                "at least one capture-device and render-device are required",
            ));
        }
        if self
            .current_capture
            .as_ref()
            .is_some_and(|device| !self.capture_devices.contains(device))
            || self
                .current_render
                .as_ref()
                .is_some_and(|device| !self.render_devices.contains(device))
        {
            return Err(script_error(
                0,
                "current device must also be listed as an available device",
            ));
        }
        Ok(FakeConfig {
            capture_devices: self.capture_devices,
            current_capture: self.current_capture,
            render_devices: self.render_devices,
            current_render: self.current_render,
            provision,
            parcel,
            participant,
            rejections: self.rejections,
        })
    }
}

fn parse_script_i32(value: &str, line: usize, field: &'static str) -> Result<i32, ProgramError> {
    value.parse().map_err(|_| ProgramError::Input {
        line,
        reason: match field {
            "parcel local ID" => "parcel local ID must be an integer",
            "reject return code" => "reject return code must be an integer",
            _ => "reject status code must be an integer",
        },
    })
}

const fn script_error(line: usize, reason: &'static str) -> ProgramError {
    ProgramError::Input { line, reason }
}

fn validate_field(field: &str, line: usize) -> Result<(), ProgramError> {
    if field.is_empty()
        || field.len() > MAX_FIELD_BYTES
        || field.chars().any(|character| {
            matches!(
                character,
                '\0'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'
            )
        })
    {
        Err(ProgramError::Input {
            line,
            reason: "fields must be non-empty, bounded UTF-8 without control characters",
        })
    } else {
        Ok(())
    }
}

async fn serve_fake(
    listener: TcpListener,
    config: FakeConfig,
) -> Result<Vec<String>, ProgramError> {
    let (stream, _) = listener.accept().await.map_err(|source| ProgramError::Io {
        action: "accepting fake Vivox connection",
        source,
    })?;
    let (reader, mut writer) = stream.into_split();
    let mut reader = BufReader::new(reader);
    let mut actions = Vec::new();
    loop {
        let mut line = String::new();
        let read = reader
            .read_line(&mut line)
            .await
            .map_err(|source| ProgramError::Io {
                action: "reading fake Vivox request",
                source,
            })?;
        if read == 0 {
            return Err(ProgramError::FakeServer(
                "client disconnected before connector shutdown",
            ));
        }
        if line.len() > MAX_CONTROL_FRAME_BYTES {
            return Err(ProgramError::FakeServer("request exceeded 1 MiB"));
        }
        let line = line.trim_matches(['\r', '\n', '\0']);
        if line.is_empty() {
            continue;
        }
        if line.contains("<!DOCTYPE") || line.contains("<!ENTITY") {
            return Err(ProgramError::FakeServer("active XML is forbidden"));
        }
        let document = Document::parse(line)
            .map_err(|_| ProgramError::FakeServer("request XML was malformed"))?;
        let request = document.root_element();
        if !request.has_tag_name("Request") {
            return Err(ProgramError::FakeServer("request root was not Request"));
        }
        let request_id = request
            .attribute("requestId")
            .ok_or(ProgramError::FakeServer("request ID missing"))?;
        request_id
            .parse::<u64>()
            .map_err(|_| ProgramError::FakeServer("request ID invalid"))?;
        let action = request
            .attribute("action")
            .ok_or(ProgramError::FakeServer("request action missing"))?;
        actions.push(action.to_owned());

        if let Some(rejection) = config.rejections.get(action) {
            let response = response_xml(
                request_id,
                action,
                rejection.return_code,
                rejection.status_code,
                &rejection.status,
                "",
            );
            writer
                .write_all(response.as_bytes())
                .await
                .map_err(|source| ProgramError::Io {
                    action: "writing fake Vivox rejection",
                    source,
                })?;
            continue;
        }

        let results = fake_action_results(request, action, &config)?;
        let response = response_xml(request_id, action, 0, 0, "OK", &results);
        writer
            .write_all(response.as_bytes())
            .await
            .map_err(|source| ProgramError::Io {
                action: "writing fake Vivox response",
                source,
            })?;
        if action == "Session.Connect.1" {
            let mut event = String::from(
                "<Event type=\"ParticipantStateChangeEvent\"><StatusCode>0</StatusCode><StatusString>OK</StatusString><State>2</State><SessionHandle>session-secret</SessionHandle>",
            );
            push_xml(&mut event, "ParticipantURI", &config.participant.uri);
            push_xml(&mut event, "AccountName", &config.participant.account_name);
            push_xml(&mut event, "DisplayName", &config.participant.display_name);
            event.push_str("</Event>\n\n\n");
            writer
                .write_all(event.as_bytes())
                .await
                .map_err(|source| ProgramError::Io {
                    action: "writing fake Vivox event",
                    source,
                })?;
        }
        if action == "Connector.InitiateShutdown.1" {
            writer.shutdown().await.map_err(|source| ProgramError::Io {
                action: "closing fake Vivox connection",
                source,
            })?;
            return Ok(actions);
        }
    }
}

fn fake_action_results(
    request: roxmltree::Node<'_, '_>,
    action: &str,
    config: &FakeConfig,
) -> Result<String, ProgramError> {
    match action {
        "Aux.GetCaptureDevices.1" => Ok(devices_xml(
            "CaptureDevices",
            "CaptureDevice",
            "CurrentCaptureDevice",
            &config.capture_devices,
            config.current_capture.as_deref(),
        )),
        "Aux.GetRenderDevices.1" => Ok(devices_xml(
            "RenderDevices",
            "RenderDevice",
            "CurrentRenderDevice",
            &config.render_devices,
            config.current_render.as_deref(),
        )),
        "Connector.Create.1" => {
            let account_server = format!("https://www.{}/api2/", config.provision.voice_server);
            require_request_text(request, "AccountManagementServer", &account_server)?;
            Ok("<ConnectorHandle>connector-secret</ConnectorHandle>".into())
        }
        "Account.Login.1" => {
            require_request_text(request, "ConnectorHandle", "connector-secret")?;
            require_request_text(request, "AccountName", &config.provision.account_name)?;
            require_request_text(request, "AccountPassword", &config.provision.password)?;
            Ok("<AccountHandle>account-secret</AccountHandle>".into())
        }
        "Session.Create.1" => {
            require_request_text(request, "AccountHandle", "account-secret")?;
            require_request_text(request, "URI", &config.parcel.channel_uri)?;
            Ok("<SessionHandle>session-secret</SessionHandle>".into())
        }
        "Session.Connect.1" | "Session.Terminate.1" => {
            require_request_text(request, "SessionHandle", "session-secret")?;
            Ok(String::new())
        }
        "Session.SetParticipantVolumeForMe.1" => {
            require_request_text(request, "SessionHandle", "session-secret")?;
            require_request_text(request, "ParticipantURI", &config.participant.uri)?;
            require_request_text(request, "Volume", "0")?;
            Ok(String::new())
        }
        "Account.Logout.1" => {
            require_request_text(request, "AccountHandle", "account-secret")?;
            Ok(String::new())
        }
        "Connector.InitiateShutdown.1" => {
            require_request_text(request, "ConnectorHandle", "connector-secret")?;
            Ok(String::new())
        }
        _ => Err(ProgramError::FakeServer(
            "client sent an unsupported control action",
        )),
    }
}

fn require_request_text(
    request: roxmltree::Node<'_, '_>,
    name: &str,
    expected: &str,
) -> Result<(), ProgramError> {
    let observed = request
        .children()
        .find(|node| node.has_tag_name(name))
        .and_then(|node| node.text());
    if observed == Some(expected) {
        Ok(())
    } else {
        Err(ProgramError::FakeServer(
            "request omitted or changed a required control field",
        ))
    }
}

fn response_xml(
    request_id: &str,
    action: &str,
    return_code: i32,
    status_code: i32,
    status: &str,
    results: &str,
) -> String {
    let mut response = format!(
        "<Response requestId=\"{request_id}\" action=\"{action}\"><ReturnCode>{return_code}</ReturnCode><InputXml><Request requestId=\"{request_id}\" /></InputXml><Results><StatusCode>{status_code}</StatusCode>"
    );
    push_xml(&mut response, "StatusString", status);
    response.push_str(results);
    response.push_str("</Results></Response>\n\n\n");
    response
}

fn devices_xml(
    collection: &str,
    item: &str,
    current: &str,
    devices: &[String],
    selected: Option<&str>,
) -> String {
    let mut xml = format!("<{collection}>");
    for device in devices {
        xml.push('<');
        xml.push_str(item);
        xml.push('>');
        push_xml(&mut xml, "Device", device);
        xml.push_str("</");
        xml.push_str(item);
        xml.push('>');
    }
    xml.push_str("</");
    xml.push_str(collection);
    xml.push('>');
    if let Some(selected) = selected {
        xml.push('<');
        xml.push_str(current);
        xml.push('>');
        push_xml(&mut xml, "Device", selected);
        xml.push_str("</");
        xml.push_str(current);
        xml.push('>');
    }
    xml
}

fn push_xml(output: &mut String, name: &str, value: &str) {
    output.push('<');
    output.push_str(name);
    output.push('>');
    for character in value.chars() {
        match character {
            '&' => output.push_str("&amp;"),
            '<' => output.push_str("&lt;"),
            '>' => output.push_str("&gt;"),
            '\'' => output.push_str("&apos;"),
            '"' => output.push_str("&quot;"),
            _ => output.push(character),
        }
    }
    output.push_str("</");
    output.push_str(name);
    output.push('>');
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn script_parser_requires_complete_consistent_fixture() {
        let path = std::env::temp_dir().join(format!(
            "metacrate-vivox-unit-{}-{}.tsv",
            std::process::id(),
            std::thread::current().name().unwrap_or("parser")
        ));
        std::fs::write(
            &path,
            "capture-device\tMicrophone\ncurrent-capture\tMicrophone\nrender-device\tSpeakers\ncurrent-render\tSpeakers\nprovision\tvoice-user\tfake-password\tvoice.example.test\nparcel\tFake Region\t42\tsip:channel-token@example.test\nparticipant\tparticipant-user\tAlice Resident\tsip:participant-token@example.test\n",
        )
        .unwrap();
        let config = read_fake_script(&path).unwrap();
        let _ = std::fs::remove_file(path);
        assert_eq!(config.capture_devices, ["Microphone"]);
        assert_eq!(config.parcel.local_id, 42);
        assert_eq!(config.participant.display_name, "Alice Resident");
    }

    #[test]
    fn response_builder_escapes_scripted_status() {
        let response = response_xml("1", "Account.Login.1", 1, 403, "bad <token>&", "");
        let document = Document::parse(response.trim()).unwrap();
        assert_eq!(
            document
                .descendants()
                .find(|node| node.has_tag_name("StatusString"))
                .and_then(|node| node.text()),
            Some("bad <token>&")
        );
    }
}
