{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://www.anguskit.com/schema/angusmeter/plugins/angus-websocket.json",
  "title": "AngusMeter WebSocket Step Configuration",
  "description": "Configuration for a single WebSocket step. Each step performs exactly one action (CONNECT, SEND, RECEIVE, CLOSE, PING, or the compound CONNECT_SEND_RECEIVE).",
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "enum": [
        "CONNECT",
        "SEND",
        "RECEIVE",
        "CLOSE",
        "PING",
        "CONNECT_SEND_RECEIVE"
      ],
      "description": "WebSocket action to perform. CONNECT_SEND_RECEIVE is a compound action that connects, sends a message, waits for a response, then optionally closes."
    },
    "url": {
      "type": "string",
      "description": "WebSocket endpoint URL (ws:// or wss://). Supports ${variable} interpolation. Required for CONNECT and CONNECT_SEND_RECEIVE actions."
    },
    "message": {
      "oneOf": [
        {
          "type": "string"
        },
        {
          "type": "object"
        },
        {
          "type": "array"
        },
        {
          "type": "number"
        },
        {
          "type": "boolean"
        }
      ],
      "description": "Text or binary message payload to send. Strings are sent as-is (BINARY treats them as hex); objects/arrays are JSON-serialized before sending. Supports ${variable} interpolation in strings. Required for SEND and CONNECT_SEND_RECEIVE actions."
    },
    "messageType": {
      "type": "string",
      "enum": [
        "TEXT",
        "BINARY"
      ],
      "default": "TEXT",
      "description": "Whether to send the message as a WebSocket text frame or binary frame."
    },
    "subprotocols": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "List of WebSocket subprotocols to negotiate (Sec-WebSocket-Protocol header), e.g. [\"graphql-ws\", \"stomp\"]."
    },
    "headers": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "description": "Custom HTTP headers to send during the WebSocket handshake. Values support ${variable} interpolation."
    },
    "connectTimeout": {
      "type": "integer",
      "minimum": 1,
      "maximum": 300,
      "default": 10,
      "description": "Maximum time in seconds to wait for the WebSocket handshake to complete."
    },
    "sendTimeout": {
      "type": "integer",
      "minimum": 1,
      "maximum": 300,
      "default": 10,
      "description": "Maximum time in seconds to wait for a sendText/sendBinary operation to complete."
    },
    "receiveTimeout": {
      "type": "integer",
      "minimum": 1,
      "maximum": 300,
      "default": 30,
      "description": "Maximum time in seconds to wait for an incoming message during RECEIVE or CONNECT_SEND_RECEIVE."
    },
    "closeTimeout": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60,
      "default": 10,
      "description": "Maximum time in seconds to wait for the CLOSE handshake to complete."
    },
    "maxQueueSize": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100000,
      "default": 1000,
      "description": "Maximum number of incoming messages to buffer in the receive queue. Oldest messages are dropped when full to prevent OOM."
    },
    "closeAfterReceive": {
      "type": "boolean",
      "default": false,
      "description": "For CONNECT_SEND_RECEIVE: if true, sends a CLOSE frame after receiving the response and clears the session from context."
    },
    "closeCode": {
      "type": "integer",
      "minimum": 1000,
      "maximum": 4999,
      "default": 1000,
      "description": "WebSocket close status code (RFC 6455). 1000 = normal closure."
    },
    "closeReason": {
      "type": "string",
      "maxLength": 123,
      "default": "normal closure",
      "description": "Human-readable close reason (max 123 bytes per RFC 6455)."
    },
    "pingPayload": {
      "type": "string",
      "maxLength": 125,
      "description": "Optional custom payload for PING frames (max 125 bytes per RFC 6455). Defaults to an auto-generated token when not specified."
    },
    "tls": {
      "type": "object",
      "description": "TLS/SSL configuration for wss:// connections.",
      "properties": {
        "trustAll": {
          "type": "boolean",
          "default": false,
          "description": "If true, accepts any server certificate including self-signed. Use only in dev/test environments. When trustAll=true, verifyHostname defaults to false as well (self-signed certs typically have mismatched CN/SANs); set verifyHostname=true explicitly to re-enable hostname checking. A WARN is logged when trustAll=true targets a non-local host."
        },
        "verifyHostname": {
          "type": "boolean",
          "description": "If false, skips hostname verification against the server certificate CN/SANs. Default: true when trustAll=false, false when trustAll=true (auto-linked unless explicitly set)."
        },
        "keystorePath": {
          "type": "string",
          "description": "Path to the client KeyStore file (for mTLS). Typically a .jks or .p12 file; store format is inferred from the extension."
        },
        "keystorePassword": {
          "type": "string",
          "description": "Password for the client KeyStore. Required when keystorePath is set."
        },
        "truststorePath": {
          "type": "string",
          "description": "Path to a custom TrustStore containing trusted CA certificates."
        },
        "truststorePassword": {
          "type": "string",
          "description": "Password for the TrustStore. Required when truststorePath is set."
        },
        "protocols": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Enabled TLS protocol versions (e.g. [\"TLSv1.3\", \"TLSv1.2\"])."
        },
        "ciphers": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Enabled cipher suites (JSSE names)."
        },
        "certPath": {
          "type": "string",
          "description": "PEM client certificate path (use with keyPath for PEM-based mTLS; alternative to keystorePath)."
        },
        "keyPath": {
          "type": "string",
          "description": "PEM PKCS#8 private key path (used with certPath)."
        },
        "keyPassword": {
          "type": "string",
          "description": "Password for encrypted PEM private key (PKCS#8 EncryptedPrivateKeyInfo).",
          "writeOnly": true
        },
        "caPath": {
          "type": "string",
          "description": "PEM CA bundle path (alternative to truststorePath)."
        }
      }
    },
    "auth": {
      "type": "object",
      "description": "Authentication shortcut that injects an Authorization header during handshake.",
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "BEARER",
            "BASIC"
          ],
          "description": "Authentication scheme."
        },
        "token": {
          "type": "string",
          "description": "Bearer token (required when type=BEARER). Supports ${variable} interpolation.",
          "writeOnly": true
        },
        "username": {
          "type": "string",
          "description": "Username (required when type=BASIC). Supports ${variable} interpolation."
        },
        "password": {
          "type": "string",
          "description": "Password (required when type=BASIC). Supports ${variable} interpolation.",
          "writeOnly": true
        }
      }
    },
    "maxRetries": {
      "type": "integer",
      "minimum": 0,
      "maximum": 20,
      "default": 0,
      "description": "Maximum number of additional connection attempts after the first failure. Applies to CONNECT and CONNECT_SEND_RECEIVE."
    },
    "retryBackoffMs": {
      "type": "integer",
      "minimum": 0,
      "maximum": 60000,
      "default": 500,
      "description": "Fixed delay in milliseconds between connection retry attempts."
    },
    "cookiePersistent": {
      "type": "boolean",
      "default": true,
      "description": "If true, shares a CookieManager across steps in the same SampleContext so Set-Cookie from one step is sent on subsequent handshakes."
    },
    "receiveCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10000,
      "default": 1,
      "description": "Number of matching messages to collect before RECEIVE returns. The last message becomes responseBody; all are available in metadata.messages when debug=true."
    },
    "drainUntilTimeout": {
      "type": "boolean",
      "default": false,
      "description": "If true, keep receiving until receiveTimeout elapses, ignoring receiveCount. Useful for collecting all messages in a streaming burst."
    },
    "receiveFilter": {
      "type": "object",
      "description": "Optional filter applied to each incoming message during RECEIVE. Non-matching messages are skipped (but still dequeued from the buffer). Exactly one of jsonPath or regex must be supplied.",
      "properties": {
        "jsonPath": {
          "type": "string",
          "description": "JSONPath expression; message matches when the expression resolves to a non-empty value."
        },
        "regex": {
          "type": "string",
          "description": "Regular expression; message matches when Pattern.find() succeeds against the raw text."
        }
      },
      "oneOf": [
        {
          "required": [
            "jsonPath"
          ]
        },
        {
          "required": [
            "regex"
          ]
        }
      ]
    },
    "maxDurationSeconds": {
      "type": "integer",
      "minimum": 1,
      "description": "Maximum duration in seconds for streaming/SSE mode. Only used with the stream() API."
    }
  },
  "required": [
    "action"
  ],
  "allOf": [
    {
      "if": {
        "properties": {
          "action": {
            "enum": [
              "CONNECT",
              "CONNECT_SEND_RECEIVE"
            ]
          }
        },
        "required": [
          "action"
        ]
      },
      "then": {
        "required": [
          "url"
        ]
      }
    },
    {
      "if": {
        "properties": {
          "action": {
            "enum": [
              "SEND",
              "CONNECT_SEND_RECEIVE"
            ]
          }
        },
        "required": [
          "action"
        ]
      },
      "then": {
        "required": [
          "message"
        ]
      }
    }
  ]
}
