use secure_exec_sidecar::generated_protocol::v1::{
    AuthenticateRequest, ConfigureVmRequest, ConnectionOwnership, ExtEnvelope, FsPermissionScope,
    GuestFilesystemCallRequest, GuestFilesystemOperation, MountDescriptor, MountPluginDescriptor,
    OwnershipScope, PermissionMode, PermissionsPolicy, ProjectedModuleDescriptor, ProtocolFrame,
    ProtocolSchema, RequestFrame, RequestPayload, ResponseFrame, ResponsePayload,
    VmConfiguredResponse, VmOwnership, WasmPermissionTier,
};
use secure_exec_sidecar::protocol as live_protocol;
use serde_json::json;
use std::collections::HashMap;

const GENERATED_AUTH_FRAME_HEX: &str = "00137365637572652d657865632d73696465636172070007000000000000000006636f6e6e2d31000e67656e6572617465642d7465737405746f6b656e070001000000";

#[test]
fn generated_protocol_round_trips_request_frame() {
    let frame = ProtocolFrame::RequestFrame(RequestFrame {
        schema: ProtocolSchema {
            name: live_protocol::PROTOCOL_NAME.to_string(),
            version: live_protocol::PROTOCOL_VERSION,
        },
        request_id: 7,
        ownership: OwnershipScope::ConnectionOwnership(ConnectionOwnership {
            connection_id: "conn-1".to_string(),
        }),
        payload: RequestPayload::AuthenticateRequest(AuthenticateRequest {
            client_name: "generated-test".to_string(),
            auth_token: "token".to_string(),
            protocol_version: live_protocol::PROTOCOL_VERSION,
            bridge_version: 1,
        }),
    });

    let encoded = serde_bare::to_vec(&frame).expect("encode generated frame");
    let decoded: ProtocolFrame = serde_bare::from_slice(&encoded).expect("decode generated frame");

    assert_eq!(decoded, frame);
}

#[test]
fn generated_protocol_matches_cross_language_auth_frame_bytes() {
    let frame = generated_auth_frame();

    let encoded = serde_bare::to_vec(&frame).expect("encode generated frame");
    assert_eq!(hex_encode(&encoded), GENERATED_AUTH_FRAME_HEX);

    let fixture = hex_decode(GENERATED_AUTH_FRAME_HEX);
    let decoded: ProtocolFrame =
        serde_bare::from_slice(&fixture).expect("decode generated auth fixture");
    assert_eq!(decoded, frame);
}

#[test]
fn live_bare_codec_matches_generated_request_bytes() {
    let codec = live_protocol::NativeFrameCodec::with_payload_codec(
        1024 * 1024,
        live_protocol::NativePayloadCodec::Bare,
    );

    let live_auth = live_protocol::ProtocolFrame::Request(live_protocol::RequestFrame::new(
        7,
        live_protocol::OwnershipScope::connection("conn-1"),
        live_protocol::RequestPayload::Authenticate(live_protocol::AuthenticateRequest {
            client_name: "generated-test".to_string(),
            auth_token: "token".to_string(),
            protocol_version: live_protocol::PROTOCOL_VERSION,
            bridge_version: 1,
        }),
    ));
    let live_auth_payload = live_frame_payload(&codec.encode(&live_auth).expect("encode auth"));
    let generated_auth_payload =
        serde_bare::to_vec(&generated_auth_frame()).expect("encode generated auth");
    assert_eq!(live_auth_payload, generated_auth_payload);

    let live_configure = live_protocol::ProtocolFrame::Request(live_protocol::RequestFrame::new(
        9,
        live_protocol::OwnershipScope::vm("conn-1", "session-1", "vm-1"),
        live_protocol::RequestPayload::ConfigureVm(live_protocol::ConfigureVmRequest {
            mounts: vec![live_protocol::MountDescriptor {
                guest_path: "/node_modules".to_string(),
                read_only: true,
                plugin: live_protocol::MountPluginDescriptor {
                    id: "host_dir".to_string(),
                    config: json!({
                        "hostPath": "/tmp/deps",
                        "readOnly": true,
                    })
                    .to_string(),
                },
            }],
            software: Vec::new(),
            permissions: Some(live_protocol::PermissionsPolicy {
                fs: Some(live_protocol::FsPermissionScope::PermissionMode(
                    live_protocol::PermissionMode::Allow,
                )),
                network: None,
                child_process: None,
                process: None,
                env: None,
                binding: None,
            }),
            module_access_cwd: Some("/workspace".to_string()),
            instructions: vec!["keep it generic".to_string()],
            projected_modules: vec![live_protocol::ProjectedModuleDescriptor {
                package_name: "workspace".to_string(),
                entrypoint: "/workspace/index.js".to_string(),
            }],
            command_permissions: std::collections::HashMap::from([(
                "cat".to_string(),
                live_protocol::WasmPermissionTier::ReadOnly,
            )]),
            loopback_exempt_ports: vec![3000],
        }),
    ));
    let live_configure_payload =
        live_frame_payload(&codec.encode(&live_configure).expect("encode configure"));
    let generated_configure_payload =
        serde_bare::to_vec(&generated_configure_frame()).expect("encode generated configure");
    assert_eq!(live_configure_payload, generated_configure_payload);

    let live_ext = live_protocol::ProtocolFrame::Request(live_protocol::RequestFrame::new(
        11,
        live_protocol::OwnershipScope::connection("conn-1"),
        live_protocol::RequestPayload::Ext(live_protocol::ExtEnvelope {
            namespace: "dev.rivet.secure-exec.test".to_string(),
            payload: b"extension-bytes".to_vec(),
        }),
    ));
    let live_ext_payload = live_frame_payload(&codec.encode(&live_ext).expect("encode ext"));
    let generated_ext_payload =
        serde_bare::to_vec(&generated_ext_frame()).expect("encode generated ext");
    assert_eq!(live_ext_payload, generated_ext_payload);
}

#[test]
fn live_bare_codec_decodes_generated_response_bytes() {
    let codec = live_protocol::NativeFrameCodec::with_payload_codec(
        1024 * 1024,
        live_protocol::NativePayloadCodec::Bare,
    );
    let generated = ProtocolFrame::ResponseFrame(ResponseFrame {
        schema: protocol_schema(),
        request_id: 9,
        ownership: generated_vm_ownership(),
        payload: ResponsePayload::VmConfiguredResponse(VmConfiguredResponse {
            applied_mounts: 2,
            applied_software: 0,
        }),
    });
    let payload = serde_bare::to_vec(&generated).expect("encode generated response");
    let decoded = codec
        .decode(&framed_payload(&payload))
        .expect("decode generated response with live codec");

    assert_eq!(
        decoded,
        live_protocol::ProtocolFrame::Response(live_protocol::ResponseFrame::new(
            9,
            live_protocol::OwnershipScope::vm("conn-1", "session-1", "vm-1"),
            live_protocol::ResponsePayload::VmConfigured(live_protocol::VmConfiguredResponse {
                applied_mounts: 2,
                applied_software: 0,
            }),
        )),
    );
}

#[test]
fn generated_protocol_preserves_json_utf8_strings() {
    let descriptor = MountPluginDescriptor {
        id: "chunked_s3".to_string(),
        config: r#"{"bucket":"demo","prefix":"workspace"}"#.to_string(),
    };

    let encoded = serde_bare::to_vec(&descriptor).expect("encode generated descriptor");
    let decoded: MountPluginDescriptor =
        serde_bare::from_slice(&encoded).expect("decode generated descriptor");

    assert_eq!(decoded, descriptor);
}

#[test]
fn generated_protocol_preserves_guest_filesystem_call_offsets() {
    let request = GuestFilesystemCallRequest {
        operation: GuestFilesystemOperation::Pread,
        path: "/workspace/data.bin".to_string(),
        destination_path: None,
        target: None,
        content: None,
        encoding: None,
        recursive: false,
        mode: None,
        uid: None,
        gid: None,
        atime_ms: None,
        mtime_ms: None,
        len: Some(12),
        offset: Some(34),
    };

    let encoded = serde_bare::to_vec(&request).expect("encode generated filesystem call");
    let decoded: GuestFilesystemCallRequest =
        serde_bare::from_slice(&encoded).expect("decode generated filesystem call");

    assert_eq!(decoded, request);
}

fn hex_encode(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        output.push(HEX[(byte >> 4) as usize] as char);
        output.push(HEX[(byte & 0x0f) as usize] as char);
    }
    output
}

fn hex_decode(hex: &str) -> Vec<u8> {
    assert_eq!(hex.len() % 2, 0, "hex length must be even");
    hex.as_bytes()
        .chunks_exact(2)
        .map(|pair| (hex_nibble(pair[0]) << 4) | hex_nibble(pair[1]))
        .collect()
}

fn generated_auth_frame() -> ProtocolFrame {
    ProtocolFrame::RequestFrame(RequestFrame {
        schema: protocol_schema(),
        request_id: 7,
        ownership: OwnershipScope::ConnectionOwnership(ConnectionOwnership {
            connection_id: "conn-1".to_string(),
        }),
        payload: RequestPayload::AuthenticateRequest(AuthenticateRequest {
            client_name: "generated-test".to_string(),
            auth_token: "token".to_string(),
            protocol_version: live_protocol::PROTOCOL_VERSION,
            bridge_version: 1,
        }),
    })
}

fn generated_configure_frame() -> ProtocolFrame {
    ProtocolFrame::RequestFrame(RequestFrame {
        schema: protocol_schema(),
        request_id: 9,
        ownership: generated_vm_ownership(),
        payload: RequestPayload::ConfigureVmRequest(ConfigureVmRequest {
            mounts: vec![MountDescriptor {
                guest_path: "/node_modules".to_string(),
                read_only: true,
                plugin: MountPluginDescriptor {
                    id: "host_dir".to_string(),
                    config: r#"{"hostPath":"/tmp/deps","readOnly":true}"#.to_string(),
                },
            }],
            software: Vec::new(),
            permissions: Some(PermissionsPolicy {
                fs: Some(FsPermissionScope::PermissionMode(PermissionMode::Allow)),
                network: None,
                child_process: None,
                process: None,
                env: None,
                binding: None,
            }),
            module_access_cwd: Some("/workspace".to_string()),
            instructions: vec!["keep it generic".to_string()],
            projected_modules: vec![ProjectedModuleDescriptor {
                package_name: "workspace".to_string(),
                entrypoint: "/workspace/index.js".to_string(),
            }],
            command_permissions: HashMap::from([("cat".to_string(), WasmPermissionTier::ReadOnly)]),
            loopback_exempt_ports: vec![3000],
        }),
    })
}

fn protocol_schema() -> ProtocolSchema {
    ProtocolSchema {
        name: live_protocol::PROTOCOL_NAME.to_string(),
        version: live_protocol::PROTOCOL_VERSION,
    }
}

fn generated_ext_frame() -> ProtocolFrame {
    ProtocolFrame::RequestFrame(RequestFrame {
        schema: protocol_schema(),
        request_id: 11,
        ownership: OwnershipScope::ConnectionOwnership(ConnectionOwnership {
            connection_id: "conn-1".to_string(),
        }),
        payload: RequestPayload::ExtEnvelope(ExtEnvelope {
            namespace: "dev.rivet.secure-exec.test".to_string(),
            payload: b"extension-bytes".to_vec(),
        }),
    })
}

fn generated_vm_ownership() -> OwnershipScope {
    OwnershipScope::VmOwnership(VmOwnership {
        connection_id: "conn-1".to_string(),
        session_id: "session-1".to_string(),
        vm_id: "vm-1".to_string(),
    })
}

fn live_frame_payload(frame: &[u8]) -> Vec<u8> {
    frame[4..].to_vec()
}

fn framed_payload(payload: &[u8]) -> Vec<u8> {
    let mut frame = Vec::with_capacity(4 + payload.len());
    frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
    frame.extend_from_slice(payload);
    frame
}

fn hex_nibble(byte: u8) -> u8 {
    match byte {
        b'0'..=b'9' => byte - b'0',
        b'a'..=b'f' => byte - b'a' + 10,
        b'A'..=b'F' => byte - b'A' + 10,
        _ => panic!("invalid hex byte {byte}"),
    }
}
