use serde_json::Value;

use crate::{
    helpers::{
        spawn_http_server, spawn_mcp_http_server, spawn_mcp_protocol_error_server,
        spawn_mcp_sse_http_server, spawn_sse_http_server, spawn_ws_server,
    },
    support::TestWorkspace,
};

#[test]
fn run_outputs_json_report_for_graphql_requests() {
    let server_url = spawn_http_server(
        200,
        "OK",
        "application/json",
        r#"{"data":{"user":{"id":"123","name":"Ada"}},"errors":null}"#,
    );
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"GraphQL Fixture

Exercises GraphQL-over-HTTP reporting.

---

Fetch GraphQL fixture

protocol = graphql
POST {server_url}
operation = GetUser
variables = {{"id":"123"}}

~~~graphql
query GetUser($id: ID!) {{
  user(id: $id) {{
    id
    name
  }}
}}
~~~

^ & graphql.errors == null
^ & graphql.data.user.id == "123"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "graphql");
    assert_eq!(
        parsed["records"][0]["protocolContext"]["operationName"],
        "GetUser"
    );
    assert_eq!(parsed["records"][0]["protocolContext"]["variables"]["id"], "123");
    assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][0]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_sse_requests() {
    let server_url = spawn_sse_http_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"SSE Fixture

Exercises SSE open and receive execution.

---

Open stream

protocol = sse
session = prices
GET {server_url}

^ & status == 200

---

Receive update

session = prices
receive
within = 2s

& sse.id -> $EVENT_ID

^ & sse.event == "price"
^ $EVENT_ID == "evt-1"
^ & body.symbol == "AAPL"
^ & body.price === NUMBER
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "sse");
    assert_eq!(parsed["records"][0]["status"], 200);
    assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
    assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "prices");
    assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");

    assert_eq!(parsed["records"][1]["protocol"], "sse");
    assert_eq!(parsed["records"][1]["protocolContext"]["action"], "receive");
    assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "prices");
    assert_eq!(parsed["records"][1]["protocolContext"]["within"], "2s");
    assert_eq!(parsed["records"][1]["protocolContext"]["event"], "price");
    assert_eq!(parsed["records"][1]["protocolContext"]["id"], "evt-1");
    assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][2]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][3]["status"], "passed");
    assert!(parsed["records"][1]["body"]
        .as_str()
        .expect("body should be serialized as a string")
        .contains("\"symbol\":\"AAPL\""));
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_ws_requests() {
    let server_url = spawn_ws_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"WS Fixture

Exercises WebSocket open, send, and receive execution.

---

Open socket

protocol = ws
session = chat
GET {server_url}

^ & status == 101

---

Send hello

session = chat
~~~json
{{"type":"hello","room":"prices"}}
~~~

^ & status == 101

---

Receive reply

session = chat
receive
within = 2s

& ws.kind -> $KIND

^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
^ & body.room == "prices"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "ws");
    assert_eq!(parsed["records"][0]["status"], 101);
    assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
    assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "chat");
    assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");

    assert_eq!(parsed["records"][1]["protocol"], "ws");
    assert_eq!(parsed["records"][1]["status"], 101);
    assert_eq!(parsed["records"][1]["protocolContext"]["action"], "send");
    assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "chat");
    assert_eq!(parsed["records"][1]["protocolContext"]["kind"], "json");
    assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");

    assert_eq!(parsed["records"][2]["protocol"], "ws");
    assert_eq!(parsed["records"][2]["status"], 101);
    assert_eq!(parsed["records"][2]["protocolContext"]["action"], "receive");
    assert_eq!(parsed["records"][2]["protocolContext"]["sessionName"], "chat");
    assert_eq!(parsed["records"][2]["protocolContext"]["within"], "2s");
    assert_eq!(parsed["records"][2]["protocolContext"]["kind"], "text");
    assert_eq!(parsed["records"][2]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][2]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["records"][2]["assertions"][2]["status"], "passed");
    assert_eq!(parsed["records"][2]["assertions"][3]["status"], "passed");
    assert!(parsed["records"][2]["body"]
        .as_str()
        .expect("body should be serialized as a string")
        .contains("\"type\":\"ack\""));
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_ws_exchange_requests() {
    let server_url = spawn_ws_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"WS Exchange Fixture

Exercises WebSocket open and exchange execution.

---

Open socket

protocol = ws
session = chat
GET {server_url}

^ & status == 101

---

Send hello

session = chat
within = 2s
~~~json
{{"type":"hello","room":"prices"}}
~~~

& ws.kind -> $KIND

^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
^ & body.room == "prices"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "ws");
    assert_eq!(parsed["records"][0]["status"], 101);
    assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
    assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "chat");
    assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");

    assert_eq!(parsed["records"][1]["protocol"], "ws");
    assert_eq!(parsed["records"][1]["status"], 101);
    assert_eq!(parsed["records"][1]["protocolContext"]["action"], "exchange");
    assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "chat");
    assert_eq!(parsed["records"][1]["protocolContext"]["within"], "2s");
    assert_eq!(parsed["records"][1]["protocolContext"]["kind"], "text");
    assert!(parsed["records"][1]["timing"]["totalMs"].as_u64().is_some());
    assert_eq!(parsed["records"][1]["timing"]["phases"][0]["name"], "send");
    assert!(parsed["records"][1]["timing"]["phases"][0]["durationMs"].as_u64().is_some());
    assert_eq!(parsed["records"][1]["timing"]["phases"][1]["name"], "wait");
    assert!(parsed["records"][1]["timing"]["phases"][1]["durationMs"].as_u64().is_some());
    assert_eq!(parsed["records"][1]["transcripts"][0]["direction"], "outgoing");
    assert_eq!(parsed["records"][1]["transcripts"][0]["label"], "ws.exchange");
    assert_eq!(parsed["records"][1]["transcripts"][0]["attributes"]["action"], "exchange");
    assert_eq!(parsed["records"][1]["transcripts"][1]["direction"], "incoming");
    assert_eq!(parsed["records"][1]["transcripts"][1]["label"], "ws.message");
    assert_eq!(parsed["records"][1]["transcripts"][1]["attributes"]["kind"], "text");
    assert_eq!(parsed["records"][1]["retainedArtifacts"], serde_json::json!([]));
    assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][2]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][3]["status"], "passed");
    assert!(parsed["records"][1]["body"]
        .as_str()
        .expect("body should be serialized as a string")
        .contains("\"type\":\"ack\""));
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_mcp_requests() {
    let server_url = spawn_mcp_http_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"MCP Fixture

Exercises HTTP MCP target execution.

---

Connect MCP

protocol = mcp
session = app
POST {server_url}
* Authorization = Bearer test-token
call = initialize

^ & body.result.serverInfo.name == "fixture-mcp"

---

List tools

protocol = mcp
session = app
POST {server_url}
call = tools/list

^ & body.result.tools[0].name == "search"

---

List resources

protocol = mcp
session = app
POST {server_url}
call = resources/list

^ & body.result.resources[0].name == "Fixture Resource"

---

Call tool

protocol = mcp
session = app
POST {server_url}
call = tools/call
tool = search
arguments = {{"query":"hedgehog"}}

^ & body.result.content[0].text == "search result"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "mcp");
    assert_eq!(parsed["records"][0]["protocolContext"]["call"], "initialize");
    assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "app");
    assert_eq!(parsed["records"][1]["protocol"], "mcp");
    assert_eq!(parsed["records"][1]["protocolContext"]["call"], "tools/list");
    assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "app");
    assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][2]["protocol"], "mcp");
    assert_eq!(parsed["records"][2]["protocolContext"]["call"], "resources/list");
    assert_eq!(parsed["records"][2]["protocolContext"]["sessionName"], "app");
    assert_eq!(parsed["records"][2]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][3]["protocol"], "mcp");
    assert_eq!(parsed["records"][3]["protocolContext"]["call"], "tools/call");
    assert_eq!(parsed["records"][3]["protocolContext"]["sessionName"], "app");
    assert_eq!(parsed["records"][3]["protocolContext"]["tool"], "search");
    assert_eq!(parsed["records"][3]["protocolContext"]["arguments"]["query"], "hedgehog");
    assert_eq!(parsed["records"][3]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_mcp_protocol_errors() {
    let server_url = spawn_mcp_protocol_error_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"MCP Error Fixture

Exercises assertions against JSON-RPC error envelopes.

---

Connect MCP

protocol = mcp
session = app
POST {server_url}
* Authorization = Bearer test-token
call = initialize

^ & body.result.serverInfo.name == "fixture-mcp"

---

Call missing tool

protocol = mcp
session = app
POST {server_url}
call = tools/call
tool = missing-tool
arguments = {{}}

^ & body.error.code == -32601
^ & body.error.message == "Tool not found"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "all", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    assert_eq!(parsed["records"][0]["protocol"], "mcp");
    assert_eq!(parsed["records"][1]["protocol"], "mcp");
    assert_eq!(parsed["records"][1]["protocolContext"]["call"], "tools/call");
    assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "app");
    assert_eq!(parsed["records"][1]["protocolContext"]["tool"], "missing-tool");
    assert_eq!(parsed["records"][1]["protocolContext"]["arguments"], serde_json::json!({}));
    assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["failures"], serde_json::json!([]));
}

#[test]
fn run_outputs_json_report_for_mcp_sse_responses() {
    let server_url = spawn_mcp_sse_http_server();
    let workspace = TestWorkspace::new();
    workspace.write_file(
        "collection.hen",
        &format!(
            r#"MCP SSE Fixture

Exercises SSE-wrapped MCP JSON-RPC responses.

---

Connect MCP

protocol = mcp
session = app
POST {server_url}
call = initialize

& body.result -> $INIT_RESULT

^ $INIT_RESULT ~= /protocolVersion/
^ & body.result.protocolVersion == "2025-06-18"
^ & body.result.serverInfo.name == "fixture-mcp"
"#
        ),
    );

    let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);

    assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
    assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);

    let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
    let body = parsed["records"][0]["body"]
        .as_str()
        .expect("body should be serialized as a string");

    assert!(body.contains("\"protocolVersion\":\"2025-06-18\""), "body: {body}");
    assert!(!body.contains("event:"), "body: {body}");
    assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
    assert_eq!(parsed["records"][0]["assertions"][1]["status"], "passed");
    assert_eq!(parsed["records"][0]["assertions"][2]["status"], "passed");
    assert_eq!(parsed["failures"], serde_json::json!([]));
}