use serde_json::json;
use shelly::{
    ChartAnnotation, ChartPoint, ClientMessage, DynamicSlotPatch, GridColumn, GridPinned, GridRow,
    GridRowsWindow, GridSavedView, GridSort, GridSortDirection, GridState, InboxItem,
    JsInteropDispatch, ServerMessage, StreamBatchOperation, StreamPosition, Toast, ToastLevel,
};
use std::{fs, path::PathBuf};

fn read_fixture(path: &str) -> serde_json::Value {
    let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(path);
    let source = fs::read_to_string(&fixture_path)
        .unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", fixture_path.display()));
    serde_json::from_str(&source)
        .unwrap_or_else(|err| panic!("failed to parse fixture {}: {err}", fixture_path.display()))
}

#[test]
fn protocol_event_shape_is_stable() {
    let message: ClientMessage = serde_json::from_value(json!({
        "type": "event",
        "event": "submit",
        "target": "profile-form",
        "value": {"name": "Ada"},
        "metadata": {"tag": "FORM"}
    }))
    .unwrap();

    match message {
        ClientMessage::Event {
            event,
            target,
            value,
            metadata,
        } => {
            assert_eq!(event, "submit");
            assert_eq!(target.as_deref(), Some("profile-form"));
            assert_eq!(value["name"], "Ada");
            assert_eq!(metadata["tag"], "FORM");
        }
        other => panic!("unexpected message: {other:?}"),
    }
}

#[test]
fn protocol_patch_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::Patch {
        target: "shelly-root".to_string(),
        html: "<main>ok</main>".to_string(),
        revision: 7,
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "patch",
            "target": "shelly-root",
            "html": "<main>ok</main>",
            "revision": 7
        })
    );
}

#[test]
fn protocol_connect_shape_is_stable() {
    let message: ClientMessage = serde_json::from_value(json!({
        "type": "connect",
        "protocol": "shelly/1"
    }))
    .unwrap();

    assert_eq!(
        message,
        ClientMessage::Connect {
            protocol: "shelly/1".to_string(),
            session_id: None,
            last_revision: None,
            resume_token: None,
            tenant_id: None,
            trace_id: None,
            span_id: None,
            parent_span_id: None,
            correlation_id: None,
            request_id: None,
        }
    );
}

#[test]
fn protocol_connect_resume_shape_is_stable() {
    let message: ClientMessage = serde_json::from_value(json!({
        "type": "connect",
        "protocol": "shelly/1",
        "session_id": "sid-1",
        "last_revision": 12,
        "resume_token": "resume-token-1"
    }))
    .unwrap();

    assert_eq!(
        message,
        ClientMessage::Connect {
            protocol: "shelly/1".to_string(),
            session_id: Some("sid-1".to_string()),
            last_revision: Some(12),
            resume_token: Some("resume-token-1".to_string()),
            tenant_id: None,
            trace_id: None,
            span_id: None,
            parent_span_id: None,
            correlation_id: None,
            request_id: None,
        }
    );
}

#[test]
fn protocol_diff_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::Diff {
        target: "shelly-root".to_string(),
        revision: 8,
        slots: vec![DynamicSlotPatch {
            index: 0,
            html: "1".to_string(),
        }],
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "diff",
            "target": "shelly-root",
            "revision": 8,
            "slots": [{"index": 0, "html": "1"}]
        })
    );
}

#[test]
fn protocol_patch_url_shape_is_stable() {
    let message: ClientMessage = serde_json::from_value(json!({
        "type": "patch_url",
        "to": "/pages/docs"
    }))
    .unwrap();

    assert_eq!(
        message,
        ClientMessage::PatchUrl {
            to: "/pages/docs".to_string()
        }
    );

    assert_eq!(
        serde_json::to_value(ServerMessage::PatchUrl {
            to: "/pages/docs".to_string()
        })
        .unwrap(),
        json!({"type": "patch_url", "to": "/pages/docs"})
    );
}

#[test]
fn protocol_navigate_shape_is_stable() {
    let message: ClientMessage = serde_json::from_value(json!({
        "type": "navigate",
        "to": "/users/42"
    }))
    .unwrap();

    assert_eq!(
        message,
        ClientMessage::Navigate {
            to: "/users/42".to_string()
        }
    );

    assert_eq!(
        serde_json::to_value(ServerMessage::Navigate {
            to: "/users/42".to_string()
        })
        .unwrap(),
        json!({"type": "navigate", "to": "/users/42"})
    );
}

#[test]
fn protocol_stream_insert_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::StreamInsert {
        target: "messages".to_string(),
        id: "msg-1".to_string(),
        html: "<li id=\"msg-1\">Hi</li>".to_string(),
        at: StreamPosition::Append,
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "stream_insert",
            "target": "messages",
            "id": "msg-1",
            "html": "<li id=\"msg-1\">Hi</li>",
            "at": "append"
        })
    );
}

#[test]
fn protocol_stream_delete_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::StreamDelete {
        target: "messages".to_string(),
        id: "msg-1".to_string(),
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "stream_delete",
            "target": "messages",
            "id": "msg-1"
        })
    );
}

#[test]
fn protocol_stream_batch_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::StreamBatch {
        target: "messages".to_string(),
        operations: vec![
            StreamBatchOperation::Insert {
                id: "msg-2".to_string(),
                html: "<li id=\"msg-2\">Batch</li>".to_string(),
                at: StreamPosition::Append,
            },
            StreamBatchOperation::Delete {
                id: "msg-1".to_string(),
            },
        ],
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "stream_batch",
            "target": "messages",
            "operations": [
                {
                    "op": "insert",
                    "id": "msg-2",
                    "html": "<li id=\"msg-2\">Batch</li>",
                    "at": "append"
                },
                {
                    "op": "delete",
                    "id": "msg-1"
                }
            ]
        })
    );
}

#[test]
fn protocol_chart_series_append_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::ChartSeriesAppend {
        chart: "traffic-chart".to_string(),
        series: "requests".to_string(),
        point: ChartPoint { x: 5.0, y: 14.0 },
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "chart_series_append",
            "chart": "traffic-chart",
            "series": "requests",
            "point": {
                "x": 5.0,
                "y": 14.0
            }
        })
    );
}

#[test]
fn protocol_chart_series_append_many_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::ChartSeriesAppendMany {
        chart: "traffic-chart".to_string(),
        series: "requests".to_string(),
        points: vec![
            ChartPoint { x: 5.0, y: 14.0 },
            ChartPoint { x: 6.0, y: 15.2 },
        ],
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "chart_series_append_many",
            "chart": "traffic-chart",
            "series": "requests",
            "points": [
                {"x": 5.0, "y": 14.0},
                {"x": 6.0, "y": 15.2}
            ]
        })
    );
}

#[test]
fn protocol_chart_annotation_shapes_are_stable() {
    let upsert = serde_json::to_value(ServerMessage::ChartAnnotationUpsert {
        chart: "traffic-chart".to_string(),
        annotation: ChartAnnotation {
            id: "release-1".to_string(),
            x: 18.0,
            label: "Deploy".to_string(),
        },
    })
    .unwrap();
    assert_eq!(
        upsert,
        json!({
            "type": "chart_annotation_upsert",
            "chart": "traffic-chart",
            "annotation": {
                "id": "release-1",
                "x": 18.0,
                "label": "Deploy"
            }
        })
    );

    let delete = serde_json::to_value(ServerMessage::ChartAnnotationDelete {
        chart: "traffic-chart".to_string(),
        id: "release-1".to_string(),
    })
    .unwrap();
    assert_eq!(
        delete,
        json!({
            "type": "chart_annotation_delete",
            "chart": "traffic-chart",
            "id": "release-1"
        })
    );
}

#[test]
fn protocol_chart_series_replace_and_reset_shapes_are_stable() {
    let replace = serde_json::to_value(ServerMessage::ChartSeriesReplace {
        chart: "traffic-chart".to_string(),
        series: "requests".to_string(),
        points: vec![
            ChartPoint { x: 10.0, y: 88.0 },
            ChartPoint { x: 11.0, y: 90.5 },
        ],
    })
    .unwrap();
    assert_eq!(
        replace,
        json!({
            "type": "chart_series_replace",
            "chart": "traffic-chart",
            "series": "requests",
            "points": [
                {"x": 10.0, "y": 88.0},
                {"x": 11.0, "y": 90.5}
            ]
        })
    );

    let reset = serde_json::to_value(ServerMessage::ChartReset {
        chart: "traffic-chart".to_string(),
    })
    .unwrap();
    assert_eq!(
        reset,
        json!({
            "type": "chart_reset",
            "chart": "traffic-chart"
        })
    );
}

#[test]
fn protocol_toast_and_inbox_shapes_are_stable() {
    let toast_push = serde_json::to_value(ServerMessage::ToastPush {
        toast: Toast {
            id: "toast-1".to_string(),
            level: ToastLevel::Warning,
            title: Some("Heads up".to_string()),
            message: "Disk nearing limit".to_string(),
            ttl_ms: Some(4000),
        },
    })
    .unwrap();
    assert_eq!(
        toast_push,
        json!({
            "type": "toast_push",
            "toast": {
                "id": "toast-1",
                "level": "warning",
                "title": "Heads up",
                "message": "Disk nearing limit",
                "ttl_ms": 4000
            }
        })
    );

    let toast_dismiss = serde_json::to_value(ServerMessage::ToastDismiss {
        id: "toast-1".to_string(),
    })
    .unwrap();
    assert_eq!(
        toast_dismiss,
        json!({
            "type": "toast_dismiss",
            "id": "toast-1"
        })
    );

    let inbox_upsert = serde_json::to_value(ServerMessage::InboxUpsert {
        item: InboxItem {
            id: "msg-1".to_string(),
            title: "Welcome".to_string(),
            body: "Thanks for joining".to_string(),
            read: false,
            inserted_at: Some("2026-05-05T12:00:00Z".to_string()),
        },
    })
    .unwrap();
    assert_eq!(
        inbox_upsert,
        json!({
            "type": "inbox_upsert",
            "item": {
                "id": "msg-1",
                "title": "Welcome",
                "body": "Thanks for joining",
                "read": false,
                "inserted_at": "2026-05-05T12:00:00Z"
            }
        })
    );

    let inbox_delete = serde_json::to_value(ServerMessage::InboxDelete {
        id: "msg-1".to_string(),
    })
    .unwrap();
    assert_eq!(
        inbox_delete,
        json!({
            "type": "inbox_delete",
            "id": "msg-1"
        })
    );
}

#[test]
fn protocol_upload_shape_is_stable() {
    let start: ClientMessage = serde_json::from_value(json!({
        "type": "upload_start",
        "upload_id": "up-1",
        "event": "uploaded",
        "target": "file",
        "name": "notes.txt",
        "size": 5,
        "content_type": "text/plain"
    }))
    .unwrap();

    assert_eq!(
        start,
        ClientMessage::UploadStart {
            upload_id: "up-1".to_string(),
            event: "uploaded".to_string(),
            target: Some("file".to_string()),
            name: "notes.txt".to_string(),
            size: 5,
            content_type: Some("text/plain".to_string()),
        }
    );

    assert_eq!(
        serde_json::to_value(ServerMessage::UploadProgress {
            upload_id: "up-1".to_string(),
            received: 5,
            total: 5,
        })
        .unwrap(),
        json!({
            "type": "upload_progress",
            "upload_id": "up-1",
            "received": 5,
            "total": 5
        })
    );
}

#[test]
fn protocol_interop_dispatch_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::InteropDispatch {
        dispatch: JsInteropDispatch {
            target: Some("peer-a".to_string()),
            event: "shelly:webrtc-signal".to_string(),
            detail: json!({"kind": "candidate", "candidate": "cand-1"}),
            bubbles: true,
        },
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "interop_dispatch",
            "dispatch": {
                "target": "peer-a",
                "event": "shelly:webrtc-signal",
                "detail": {"kind": "candidate", "candidate": "cand-1"},
                "bubbles": true
            }
        })
    );
}

#[test]
fn protocol_grid_replace_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::GridReplace {
        grid: "enterprise-grid".to_string(),
        state: GridState {
            columns: vec![
                GridColumn {
                    id: "name".to_string(),
                    label: "Name".to_string(),
                    width_px: Some(220),
                    min_width_px: Some(120),
                    pinned: GridPinned::Left,
                    sortable: true,
                    resizable: true,
                    editable: false,
                },
                GridColumn {
                    id: "arr".to_string(),
                    label: "ARR".to_string(),
                    width_px: Some(120),
                    min_width_px: Some(80),
                    pinned: GridPinned::None,
                    sortable: true,
                    resizable: true,
                    editable: true,
                },
            ],
            rows: vec![GridRow {
                id: "row-1".to_string(),
                cells: std::iter::once(("name".to_string(), json!("Acme"))).collect(),
                group: Some("Enterprise".to_string()),
            }],
            total_rows: 2500,
            offset: 0,
            limit: 200,
            views: vec![GridSavedView {
                id: "ops".to_string(),
                label: "Ops".to_string(),
            }],
            active_view: Some("ops".to_string()),
            group_by: Some("segment".to_string()),
            query: Some("acme".to_string()),
            sort: Some(GridSort {
                column: "arr".to_string(),
                direction: GridSortDirection::Desc,
            }),
        },
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "grid_replace",
            "grid": "enterprise-grid",
            "state": {
                "columns": [
                    {
                        "id": "name",
                        "label": "Name",
                        "width_px": 220,
                        "min_width_px": 120,
                        "pinned": "left",
                        "sortable": true,
                        "resizable": true,
                        "editable": false
                    },
                    {
                        "id": "arr",
                        "label": "ARR",
                        "width_px": 120,
                        "min_width_px": 80,
                        "pinned": "none",
                        "sortable": true,
                        "resizable": true,
                        "editable": true
                    }
                ],
                "rows": [
                    {
                        "id": "row-1",
                        "cells": {
                            "name": "Acme"
                        },
                        "group": "Enterprise"
                    }
                ],
                "total_rows": 2500,
                "offset": 0,
                "limit": 200,
                "views": [
                    {
                        "id": "ops",
                        "label": "Ops"
                    }
                ],
                "active_view": "ops",
                "group_by": "segment",
                "query": "acme",
                "sort": {
                    "column": "arr",
                    "direction": "desc"
                }
            }
        })
    );
}

#[test]
fn protocol_grid_rows_replace_shape_is_stable() {
    let encoded = serde_json::to_value(ServerMessage::GridRowsReplace {
        grid: "enterprise-grid".to_string(),
        window: GridRowsWindow {
            offset: 400,
            total_rows: 1000,
            rows: vec![GridRow {
                id: "acct-401".to_string(),
                cells: serde_json::Map::from_iter([
                    ("name".to_string(), json!("Acme North")),
                    ("arr".to_string(), json!(166000)),
                ]),
                group: Some("Enterprise".to_string()),
            }],
        },
    })
    .unwrap();

    assert_eq!(
        encoded,
        json!({
            "type": "grid_rows_replace",
            "grid": "enterprise-grid",
            "window": {
                "offset": 400,
                "total_rows": 1000,
                "rows": [
                    {
                        "id": "acct-401",
                        "cells": {
                            "name": "Acme North",
                            "arr": 166000
                        },
                        "group": "Enterprise"
                    }
                ]
            }
        })
    );
}

#[test]
fn protocol_client_event_fixture_matches_contract() {
    let message: ClientMessage =
        serde_json::from_value(read_fixture("protocol/client_event_submit.json")).unwrap();

    assert_eq!(
        message,
        ClientMessage::Event {
            event: "submit".to_string(),
            target: Some("profile-form".to_string()),
            value: json!({"name": "Ada"}),
            metadata: serde_json::Map::from_iter([("tag".to_string(), json!("FORM"))]),
        }
    );
}

#[test]
fn protocol_patch_fixture_matches_contract() {
    let expected = read_fixture("protocol/server_patch_counter.json");
    let encoded = serde_json::to_value(ServerMessage::Patch {
        target: "shelly-root".to_string(),
        html: "<main>ok</main>".to_string(),
        revision: 7,
    })
    .unwrap();

    assert_eq!(encoded, expected);
}

#[test]
fn protocol_diff_fixture_matches_contract() {
    let expected = read_fixture("protocol/server_diff_counter.json");
    let encoded = serde_json::to_value(ServerMessage::Diff {
        target: "shelly-root".to_string(),
        revision: 8,
        slots: vec![DynamicSlotPatch {
            index: 0,
            html: "1".to_string(),
        }],
    })
    .unwrap();

    assert_eq!(encoded, expected);
}
