use http::Method;
use pest::Parser;

use crate::request::RequestProtocol;

use super::*;

#[test]
fn schema_object_declaration_rule_accepts_indented_fields() {
    let source = r#"schema User {
  id: UUID
  birthday?: DATE
}
"#;

    assert!(CollectionParser::parse(Rule::schema_object_declaration, source).is_ok());
}

#[test]
fn preamble_rule_accepts_scalar_and_schema_declarations() {
    let source = r#"
scalar Food = enum("pizza", "taco", "salad")

schema User {
  id: UUID
  birthday?: DATE
  favoriteFoods: Food[]
}

"#;

    assert!(CollectionParser::parse(Rule::preamble, source).is_ok());
}

#[test]
fn request_collection_exposes_declarations_before_requests() {
    let source = r#"
scalar Food = enum("pizza", "taco", "salad")

schema User {
  id: UUID
}

---

Get User

GET https://example.com/user/123
"#;

    let mut pairs = CollectionParser::parse(Rule::request_collection, source).unwrap();
    let collection = pairs.next().unwrap();
    let rules: Vec<_> = collection.into_inner().map(|pair| pair.as_rule()).collect();

    assert!(rules.contains(&Rule::declaration));
    assert!(rules.contains(&Rule::requests));
}

#[test]
fn parse_collection_registers_schema_declarations() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
scalar Food = enum("pizza", "taco", "salad")

schema User {
  id: UUID
  birthday?: DATE
  favoriteFoods: Food[]
}

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(matches!(
        collection.schema_registry.get("Food"),
        Some(crate::schema::RegistryEntry::Scalar(_))
    ));
    assert!(matches!(
        collection.schema_registry.get("User"),
        Some(crate::schema::RegistryEntry::Schema(_))
    ));
    assert_eq!(collection.requests.len(), 1);
}

#[test]
fn parse_collection_builds_graphql_requests() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Get User GraphQL

protocol = graphql
POST https://example.com/graphql
operation = GetUser
variables = {"id":"123"}

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

^ & body.data.user.id == "123"
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert_eq!(collection.requests.len(), 1);

    let request = &collection.requests[0];
    assert_eq!(request.protocol(), RequestProtocol::Graphql);

    let graphql = request
        .graphql_operation()
        .expect("graphql operation should exist");
    assert_eq!(graphql.http.method, Method::POST);
    assert_eq!(graphql.http.url, "https://example.com/graphql");
    assert_eq!(graphql.operation_name.as_deref(), Some("GetUser"));
    assert_eq!(graphql.variables_json.as_deref(), Some("{\"id\":\"123\"}"));
    assert!(graphql.document.contains("query GetUser($id: ID!)"));
    assert!(graphql.http.form_data.is_empty());
    assert!(graphql.http.body.is_none());
    assert!(graphql.http.body_content_type.is_none());
}

#[test]
fn inspect_collection_syntax_inherits_session_request_target() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Initialize MCP

protocol = mcp
session = app
POST https://example.com/mcp
call = initialize

---

List tools

session = app
call = tools/list
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests.len(), 2);
    assert_eq!(summary.requests[1].method, "POST");
    assert_eq!(summary.requests[1].url, "https://example.com/mcp");
    assert_eq!(summary.requests[1].protocol, "mcp");
}

#[test]
fn parse_collection_builds_sse_requests() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open stream

protocol = sse
session = prices
GET https://example.com/prices/stream

---

Receive price update

session = prices
receive
within = 5s
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert_eq!(collection.requests.len(), 2);

    let open = &collection.requests[0];
    assert_eq!(open.protocol(), RequestProtocol::Sse);
    assert_eq!(open.session_name.as_deref(), Some("prices"));
    assert_eq!(open.http_operation().method, Method::GET);
    assert_eq!(open.http_operation().url, "https://example.com/prices/stream");
    assert!(matches!(
        open.sse_operation().expect("sse operation should exist").action,
        crate::request::SseAction::Open
    ));

    let receive = &collection.requests[1];
    assert_eq!(receive.protocol(), RequestProtocol::Sse);
    assert_eq!(receive.session_name.as_deref(), Some("prices"));
    assert_eq!(receive.http_operation().method, Method::GET);
    assert_eq!(
        receive.http_operation().url,
        "https://example.com/prices/stream"
    );
    assert!(matches!(
        receive.sse_operation().expect("sse operation should exist").action,
        crate::request::SseAction::Receive { .. }
    ));
}

#[test]
fn inspect_collection_syntax_builds_sse_receive_context() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open stream

protocol = sse
session = prices
GET https://example.com/prices/stream

---

Receive price update

session = prices
receive
within = 5s
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests.len(), 2);
    assert_eq!(summary.requests[1].method, "GET");
    assert_eq!(summary.requests[1].url, "https://example.com/prices/stream");
    assert_eq!(summary.requests[1].protocol, "sse");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "receive");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["within"], "5s");
}

#[test]
fn inspect_collection_syntax_rejects_sse_receive_without_within() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Receive price update

protocol = sse
session = prices
GET https://example.com/prices/stream
receive
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("SSE receive steps require 'within = ...'"));
}

#[test]
fn inspect_collection_syntax_rejects_invalid_sse_within_duration() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Receive price update

protocol = sse
session = prices
GET https://example.com/prices/stream
receive
within = soon
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("invalid within duration 'soon'"));
}

#[test]
fn parse_collection_builds_ws_requests() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
~~~json
{"type":"hello"}
~~~

---

Receive reply

session = chat
receive
within = 2s
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert_eq!(collection.requests.len(), 3);

    let open = &collection.requests[0];
    assert_eq!(open.protocol(), RequestProtocol::Ws);
    assert!(matches!(
        open.ws_operation().expect("ws operation should exist").action,
        crate::request::WsAction::Open
    ));

    let send = &collection.requests[1];
    assert_eq!(send.protocol(), RequestProtocol::Ws);
    assert_eq!(send.http_operation().method, Method::GET);
    assert_eq!(send.http_operation().url, "wss://example.com/chat");
    assert!(matches!(
        send.ws_operation().expect("ws operation should exist").action,
        crate::request::WsAction::Send {
            kind: crate::request::WsSendKind::Json,
            ..
        }
    ));

    let receive = &collection.requests[2];
    assert_eq!(receive.protocol(), RequestProtocol::Ws);
    assert_eq!(receive.http_operation().method, Method::GET);
    assert_eq!(receive.http_operation().url, "wss://example.com/chat");
    assert!(matches!(
        receive.ws_operation().expect("ws operation should exist").action,
        crate::request::WsAction::Receive { .. }
    ));
}

#[test]
fn parse_collection_builds_ws_exchange_requests() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
within = 2s
~~~json
{"type":"hello"}
~~~
"#;

    let collection = parse_collection(source, working_dir).unwrap();
    assert_eq!(collection.requests.len(), 2);

    let exchange = &collection.requests[1];
    assert!(matches!(
        exchange.ws_operation().expect("ws operation should exist").action,
        crate::request::WsAction::Exchange {
            kind: crate::request::WsSendKind::Json,
            ..
        }
    ));
}

#[test]
fn inspect_collection_syntax_builds_ws_context() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
~~~text
hello
~~~

---

Receive reply

session = chat
receive
within = 2s
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests.len(), 3);
    assert_eq!(summary.requests[1].protocol, "ws");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "send");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["kind"], "text");
    assert_eq!(summary.requests[2].protocol, "ws");
    assert_eq!(summary.requests[2].protocol_context.as_ref().unwrap()["action"], "receive");
    assert_eq!(summary.requests[2].protocol_context.as_ref().unwrap()["within"], "2s");
}

#[test]
fn inspect_collection_syntax_builds_ws_exchange_context() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
within = 2s
~~~
hello
~~~
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "exchange");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["kind"], "text");
    assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["within"], "2s");
}

#[test]
fn inspect_collection_syntax_accepts_defaulted_prompt_variable_assignments() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
name = WebSocket Fixture
description = Demonstrates WebSocket open authoring with a defaulted prompt variable.

$ WS_ORIGIN = [[ ws_origin = wss://example.com ]]

---

Open socket

protocol = ws
session = chat
GET {{ WS_ORIGIN }}/chat
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests.len(), 1);
    assert_eq!(summary.requests[0].protocol, "ws");
    assert!(summary.requests[0].url.ends_with("/chat"));
}

#[test]
fn inspect_collection_syntax_rejects_ws_send_without_body() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
send = text
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("WebSocket send steps require a body block"));
}

#[test]
fn inspect_collection_syntax_rejects_conflicting_ws_send_kind_and_body_type() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Open socket

protocol = ws
session = chat
GET wss://example.com/chat

---

Send hello

session = chat
send = text
~~~json
{"type":"hello"}
~~~
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("conflicts with body block type 'json'"));
}

#[test]
fn inspect_collection_syntax_rejects_session_request_without_target_anchor() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

List tools

protocol = mcp
session = app
call = tools/list
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("session-backed request omitted its method/URL"));
}

#[test]
fn inspect_collection_syntax_rejects_mcp_tool_call_without_tool() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Broken MCP

protocol = mcp
POST https://example.com/mcp
call = tools/call
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err.to_string().contains("requires 'tool = ...'"));
}

#[test]
fn inspect_collection_syntax_rejects_graphql_without_graphql_block() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Broken GraphQL

protocol = graphql
POST https://example.com/graphql

~~~application/json
{"query":"query { viewer { id } }"}
~~~
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err.to_string().contains("~~~graphql document block"));
}

#[test]
fn inspect_collection_syntax_rejects_graphql_form_fields() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Broken GraphQL

protocol = graphql
POST https://example.com/graphql
~ note = hello

~~~graphql
query Viewer {
  viewer { id }
}
~~~
"#;

    let err = inspect_collection_syntax(source, working_dir).unwrap_err();

    assert!(err
        .to_string()
        .contains("GraphQL requests do not support form fields"));
}

#[test]
fn parsed_scalar_declarations_validate_values() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
scalar UserId = string & format(uuid)
scalar Status = enum("queued", "running", "done")
scalar Handle = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
scalar Age = integer & range(0..130)

---

Get User

GET https://example.com/user/123

^ & body === UserId
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(collection
        .schema_registry
        .validate_scalar_target(
            "UserId",
            &serde_json::json!("550e8400-e29b-41d4-a716-446655440000")
        )
        .is_ok());
    assert!(collection
        .schema_registry
        .validate_scalar_target("Status", &serde_json::json!("running"))
        .is_ok());
    assert!(collection
        .schema_registry
        .validate_scalar_target("Handle", &serde_json::json!("alice_1"))
        .is_ok());
    assert!(collection
        .schema_registry
        .validate_scalar_target("Age", &serde_json::json!(42))
        .is_ok());

    let user_id_error = collection
        .schema_registry
        .validate_scalar_target("UserId", &serde_json::json!("not-a-uuid"))
        .unwrap_err();
    assert!(matches!(
        user_id_error.reason,
        crate::schema::ScalarValidationErrorKind::FormatMismatch { .. }
    ));

    let handle_error = collection
        .schema_registry
        .validate_scalar_target("Handle", &serde_json::json!("ab"))
        .unwrap_err();
    assert!(matches!(
        handle_error.reason,
        crate::schema::ScalarValidationErrorKind::LengthOutOfRange { .. }
    ));

    let age_error = collection
        .schema_registry
        .validate_scalar_target("Age", &serde_json::json!(131))
        .unwrap_err();
    assert!(matches!(
        age_error.reason,
        crate::schema::ScalarValidationErrorKind::RangeOutOfRange { .. }
    ));
}

#[test]
fn parsed_scalar_declarations_preserve_named_scalar_chains() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
scalar WORD = string & pattern(/^[a-z]+$/)
scalar HANDLE = WORD & len(3..24)

---

Get User

GET https://example.com/user/123

^ & body === HANDLE
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(collection
        .schema_registry
        .validate_scalar_target("HANDLE", &serde_json::json!("alice"))
        .is_ok());

    let pattern_error = collection
        .schema_registry
        .validate_scalar_target("HANDLE", &serde_json::json!("Alice"))
        .unwrap_err();
    assert!(matches!(
        pattern_error.reason,
        crate::schema::ScalarValidationErrorKind::PatternMismatch { .. }
    ));
}

#[test]
fn parsed_schema_declarations_validate_objects() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
scalar Food = enum("pizza", "taco")

schema Address {
  city: string
  postalCode: string
}

schema User {
  id: UUID
  birthday?: DATE?
  favoriteFoods: Food[]
  address: Address
}

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(collection
        .schema_registry
        .validate_schema_target(
            "User",
            &serde_json::json!({
                "id": "550e8400-e29b-41d4-a716-446655440000",
                "birthday": null,
                "favoriteFoods": ["pizza", "taco"],
                "address": {
                    "city": "Austin",
                    "postalCode": "78701"
                },
                "extra": true
            })
        )
        .is_ok());

    let error = collection
        .schema_registry
        .validate_schema_target(
            "User",
            &serde_json::json!({
                "id": "550e8400-e29b-41d4-a716-446655440000",
                "favoriteFoods": ["pizza", "burger"],
                "address": {
                    "city": "Austin",
                    "postalCode": "78701"
                }
            })
        )
        .unwrap_err();

    assert_eq!(error.path, "$.favoriteFoods[1]");
    assert!(matches!(
        error.reason,
        crate::schema::SchemaValidationErrorKind::ScalarViolation { .. }
    ));
}

#[test]
fn parsed_schema_declarations_validate_root_array_schemas() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
schema User {
  id: UUID
}

schema Users = User[]

---

Get Users

GET https://example.com/users

^ & body === Users
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(collection
        .schema_registry
        .validate_schema_target(
            "Users",
            &serde_json::json!([
                { "id": "550e8400-e29b-41d4-a716-446655440000" },
                { "id": "123e4567-e89b-12d3-a456-426614174000" }
            ])
        )
        .is_ok());

    let error = collection
        .schema_registry
        .validate_schema_target(
            "Users",
            &serde_json::json!([
                { "id": "550e8400-e29b-41d4-a716-446655440000" },
                { "id": null }
            ])
        )
        .unwrap_err();

    assert_eq!(error.path, "$[1].id");
    assert!(matches!(
        error.reason,
        crate::schema::SchemaValidationErrorKind::TypeMismatch { .. }
    ));
}

#[test]
fn parse_collection_allows_forward_references_in_declarations() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
scalar HANDLE = WORD & len(3..24)
scalar WORD = string & pattern(/^[a-z]+$/)

schema User {
  id: HANDLE
}

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let collection = parse_collection(source, working_dir).unwrap();

    assert!(collection.schema_registry.get("HANDLE").is_some());
    assert!(collection.schema_registry.get("WORD").is_some());
    assert_eq!(collection.requests.len(), 1);
}

#[test]
fn parse_collection_rejects_declaration_after_first_request_delimiter() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
---

Get User

GET https://example.com/user/123

schema User {
  id: UUID
}
"#;

    assert!(parse_collection(source, working_dir).is_err());
}

#[test]
fn parse_collection_rejects_cyclic_schema_references() {
    let working_dir = std::env::current_dir().unwrap();
    let source = r#"
schema User {
    address: Address
}

schema Address {
    user: User
}

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let error = parse_collection(source, working_dir).unwrap_err();

    assert!(error.to_string().contains("circular reference"));
    assert!(error.to_string().contains("User"));
    assert!(error.to_string().contains("Address"));
}

#[test]
fn inspect_collection_syntax_accepts_imported_schema_declarations() {
    let temp_dir = tempfile::tempdir().unwrap();
    let working_dir = temp_dir.path().to_path_buf();
    let imported = working_dir.join("common.hen");
    std::fs::write(
        &imported,
        r#"
scalar Food = enum("pizza", "taco")

schema User {
  id: UUID
  favoriteFoods: Food[]
}
"#,
    )
    .unwrap();

    let source = r#"
<< ./common.hen

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let summary = inspect_collection_syntax(source, working_dir).unwrap();

    assert_eq!(summary.requests.len(), 1);
    assert_eq!(summary.requests[0].method, "GET");
    assert_eq!(summary.requests[0].url, "https://example.com/user/123");
}

#[test]
fn parse_collection_rejects_duplicate_imported_declaration_names() {
    let temp_dir = tempfile::tempdir().unwrap();
    let working_dir = temp_dir.path().to_path_buf();
    let imported = working_dir.join("common.hen");
    std::fs::write(
        &imported,
        r#"
schema User {
    id: UUID
}
"#,
    )
    .unwrap();

    let source = r#"
<< ./common.hen

schema User {
    id: UUID
}

---

Get User

GET https://example.com/user/123

^ & body === User
"#;

    let error = parse_collection(source, working_dir).unwrap_err();

    assert!(error.to_string().contains("User is already defined"));
}