use std::fs;
use std::path::PathBuf;

use serde_json::Value;
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
    mx_command, mx_value, MxCommand, MxCommandKind, MxCommandRequest, MxDataType, MxEvent,
    MxEventFamily, MxValue, OpenSessionReply, ProtocolStatusCode, RegisterCommand,
};
use zb_mom_ww_mxgateway_client::{GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};

#[test]
fn generated_golden_fixtures_are_available() {
    for fixture_name in [
        "open-session-reply.ok.json",
        "register-command-request.json",
        "on-data-change-event.json",
    ] {
        let fixture = read_fixture(fixture_name);
        assert!(
            fixture.is_object(),
            "{fixture_name} must remain a protobuf JSON object"
        );
    }
}

#[test]
fn open_session_fixture_matches_protocol_versions() {
    let fixture = read_fixture("open-session-reply.ok.json");
    let reply = OpenSessionReply {
        session_id: string_field(&fixture, "sessionId"),
        backend_name: string_field(&fixture, "backendName"),
        worker_process_id: i32_field(&fixture, "workerProcessId"),
        worker_protocol_version: u32_field(&fixture, "workerProtocolVersion"),
        gateway_protocol_version: u32_field(&fixture, "gatewayProtocolVersion"),
        protocol_status: Some(
            zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::ProtocolStatus {
                code: ProtocolStatusCode::Ok as i32,
                message: string_field(&fixture["protocolStatus"], "message"),
            },
        ),
        ..OpenSessionReply::default()
    };

    assert_eq!(reply.gateway_protocol_version, GATEWAY_PROTOCOL_VERSION);
    assert_eq!(reply.worker_protocol_version, WORKER_PROTOCOL_VERSION);
}

#[test]
fn register_fixture_can_build_generated_request() {
    let fixture = read_fixture("register-command-request.json");
    let command = &fixture["command"];
    let request = MxCommandRequest {
        session_id: string_field(&fixture, "sessionId"),
        client_correlation_id: string_field(&fixture, "clientCorrelationId"),
        command: Some(MxCommand {
            kind: MxCommandKind::Register as i32,
            payload: Some(mx_command::Payload::Register(RegisterCommand {
                client_name: string_field(&command["register"], "clientName"),
            })),
        }),
    };

    assert_eq!(request.session_id, "session-fixture");
    assert_eq!(
        request.command.unwrap().kind,
        MxCommandKind::Register as i32
    );
}

#[test]
fn on_data_change_fixture_can_build_generated_event() {
    let fixture = read_fixture("on-data-change-event.json");
    let event = MxEvent {
        family: MxEventFamily::OnDataChange as i32,
        session_id: string_field(&fixture, "sessionId"),
        server_handle: i32_field(&fixture, "serverHandle"),
        item_handle: i32_field(&fixture, "itemHandle"),
        value: Some(MxValue {
            data_type: MxDataType::Integer as i32,
            variant_type: string_field(&fixture["value"], "variantType"),
            kind: Some(mx_value::Kind::Int32Value(i32_field(
                &fixture["value"],
                "int32Value",
            ))),
            ..MxValue::default()
        }),
        quality: i32_field(&fixture, "quality"),
        worker_sequence: u64_field(&fixture, "workerSequence"),
        ..MxEvent::default()
    };

    assert_eq!(event.family, MxEventFamily::OnDataChange as i32);
    assert_eq!(event.value.unwrap().data_type, MxDataType::Integer as i32);
}

fn read_fixture(name: &str) -> Value {
    let path = fixture_root().join(name);
    let data = fs::read_to_string(&path).unwrap_or_else(|error| {
        panic!("failed to read fixture {}: {error}", path.display());
    });

    serde_json::from_str(&data).unwrap_or_else(|error| {
        panic!("failed to parse fixture {}: {error}", path.display());
    })
}

fn fixture_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto/fixtures/golden")
}

fn string_field(value: &Value, name: &str) -> String {
    value[name]
        .as_str()
        .unwrap_or_else(|| panic!("missing string field {name}"))
        .to_owned()
}

fn i32_field(value: &Value, name: &str) -> i32 {
    value[name]
        .as_i64()
        .unwrap_or_else(|| panic!("missing i32 field {name}"))
        .try_into()
        .unwrap_or_else(|_| panic!("field {name} does not fit in i32"))
}

fn u32_field(value: &Value, name: &str) -> u32 {
    value[name]
        .as_u64()
        .unwrap_or_else(|| panic!("missing u32 field {name}"))
        .try_into()
        .unwrap_or_else(|_| panic!("field {name} does not fit in u32"))
}

fn u64_field(value: &Value, name: &str) -> u64 {
    if let Some(number) = value[name].as_u64() {
        return number;
    }

    value[name]
        .as_str()
        .unwrap_or_else(|| panic!("missing u64 field {name}"))
        .parse()
        .unwrap_or_else(|_| panic!("field {name} does not parse as u64"))
}
