# frozen_string_literal: true

require "test_helper"

module MCP
  class ConfigurationTest < ActiveSupport::TestCase
    test "initializes with a default no-op exception reporter" do
      config = Configuration.new
      assert_respond_to config, :exception_reporter

      # The default reporter should be callable but do nothing
      exception = StandardError.new("test error")
      server_context = { test: "context" }

      assert_nothing_raised do
        config.exception_reporter.call(exception, server_context)
      end
    end

    test "allows setting a custom exception reporter" do
      config = Configuration.new
      reported_exception = nil
      reported_context = nil

      config.exception_reporter = ->(exception, server_context) do
        reported_exception = exception
        reported_context = server_context
      end

      test_exception = StandardError.new("test error")
      test_context = { foo: "bar" }

      config.exception_reporter.call(test_exception, test_context)

      assert_equal test_exception, reported_exception
      assert_equal test_context, reported_context
    end

    test "initializes with default protocol version" do
      # The unset pin reads as the version the handshake settles on, which is also the only kind of
      # value the writer accepts: reading back a version that cannot be set again would break code
      # that round-trips the setting.
      config = Configuration.new

      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, config.protocol_version
      refute_predicate config, :protocol_version?
      assert_nothing_raised { Configuration.new(protocol_version: config.protocol_version) }
    end

    test "uses the default protocol version when protocol_version is set to nil" do
      config = Configuration.new(protocol_version: nil)

      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, config.protocol_version
    end

    test "raises ArgumentError when setting the draft protocol version" do
      exception = assert_raises(ArgumentError) do
        # DRAFT-2025-v3 is the latest draft protocol version:
        # https://github.com/modelcontextprotocol/modelcontextprotocol/blob/14ec41c/schema/draft/schema.ts#L15
        Configuration.new(protocol_version: "DRAFT-2025-v3")
      end

      assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
    end

    test "raises ArgumentError when protocol_version is not a supported protocol version" do
      config = Configuration.new
      exception = assert_raises(ArgumentError) do
        custom_version = "2025-03-27"
        config.protocol_version = custom_version
      end
      assert_equal("protocol_version must be 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
    end

    test "exposes the SEP-2575 modern protocol versions" do
      assert_equal "2026-07-28", Configuration::LATEST_MODERN_PROTOCOL_VERSION
      assert_equal ["2026-07-28"], Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS
      assert Configuration.modern_protocol_version?("2026-07-28")
      refute Configuration.modern_protocol_version?("2025-11-25")
    end

    test "rejects a modern version as the protocol version pin" do
      # The pin scopes the `initialize` handshake; a modern version has no handshake to pin
      # (its version rides every request in `_meta`), so accepting it would configure nothing.
      # The message names that reason instead of listing the accepted values, because this is
      # the error an upgrade from a release that accepted the value lands on.
      exception = assert_raises(ArgumentError) do
        Configuration.new(protocol_version: Configuration::LATEST_MODERN_PROTOCOL_VERSION)
      end

      assert_includes(exception.message, "is a modern protocol version and cannot be pinned here")
      assert_includes(exception.message, "remove the setting")

      config = Configuration.new
      assert_raises(ArgumentError) { config.protocol_version = "2026-07-28" }
    end

    test "raises ArgumentError when protocol_version is not a boolean value" do
      config = Configuration.new
      exception = assert_raises(ArgumentError) do
        config.validate_tool_call_arguments = "true"
      end
      assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
    end

    test "raises ArgumentError when validate_tool_call_results is not a boolean value" do
      config = Configuration.new
      exception = assert_raises(ArgumentError) do
        config.validate_tool_call_results = "true"
      end
      assert_equal("validate_tool_call_results must be a boolean", exception.message)
    end

    test "merges protocol version from other configuration" do
      config1 = Configuration.new(protocol_version: "2025-03-26")
      config2 = Configuration.new(protocol_version: "2025-06-18")
      config3 = Configuration.new

      merged = config1.merge(config2)
      assert_equal "2025-06-18", merged.protocol_version

      merged = config1.merge(config3)
      assert_equal "2025-03-26", merged.protocol_version

      merged = config3.merge(config1)
      assert_equal "2025-03-26", merged.protocol_version
    end

    test "defaults validate_tool_call_arguments to true" do
      config = Configuration.new
      assert config.validate_tool_call_arguments
    end

    test "can set validate_tool_call_arguments to false" do
      config = Configuration.new(validate_tool_call_arguments: false)
      refute config.validate_tool_call_arguments
    end

    test "validate_tool_call_arguments? returns false when set" do
      config = Configuration.new(validate_tool_call_arguments: false)
      refute config.validate_tool_call_arguments?
    end

    test "validate_tool_call_arguments? returns true when not set" do
      config = Configuration.new
      assert config.validate_tool_call_arguments?
    end

    test "merge preserves validate_tool_call_arguments from other config" do
      config1 = Configuration.new(validate_tool_call_arguments: false)
      config2 = Configuration.new
      merged = config1.merge(config2)
      assert merged.validate_tool_call_arguments?
    end

    test "merge preserves validate_tool_call_arguments from self when other not set" do
      config1 = Configuration.new(validate_tool_call_arguments: false)
      config2 = Configuration.new
      merged = config2.merge(config1)
      refute merged.validate_tool_call_arguments
    end

    test "defaults instrument_server_context to false" do
      config = Configuration.new

      refute_predicate config, :instrument_server_context?
    end

    test "accepts instrument_server_context in the constructor" do
      config = Configuration.new(instrument_server_context: true)

      assert_predicate config, :instrument_server_context?
    end

    test "raises ArgumentError when instrument_server_context is not a boolean value" do
      config = Configuration.new

      exception = assert_raises(ArgumentError) do
        config.instrument_server_context = "true"
      end
      assert_equal("instrument_server_context must be a boolean", exception.message)
    end

    test "merge carries instrument_server_context from the other configuration" do
      base = Configuration.new(instrument_server_context: true)
      other = Configuration.new(instrument_server_context: false)

      refute_predicate base.merge(other), :instrument_server_context?
      assert_predicate other.merge(base), :instrument_server_context?
    end

    test "defaults validate_tool_call_results to false" do
      config = Configuration.new
      refute config.validate_tool_call_results
    end

    test "can set validate_tool_call_results to true" do
      config = Configuration.new(validate_tool_call_results: true)
      assert config.validate_tool_call_results
    end

    test "validate_tool_call_results? returns false when not set" do
      config = Configuration.new
      refute config.validate_tool_call_results?
    end

    test "validate_tool_call_results? returns true when set" do
      config = Configuration.new(validate_tool_call_results: true)
      assert config.validate_tool_call_results?
    end

    test "merge preserves validate_tool_call_results from other config" do
      config1 = Configuration.new(validate_tool_call_results: true)
      config2 = Configuration.new
      merged = config1.merge(config2)
      refute merged.validate_tool_call_results?
    end

    test "merge preserves validate_tool_call_results from self when other set" do
      config1 = Configuration.new(validate_tool_call_results: true)
      config2 = Configuration.new
      merged = config2.merge(config1)
      assert merged.validate_tool_call_results
    end

    test "initializes with a default pass-through around_request" do
      config = Configuration.new
      called = false
      config.around_request.call({}) { called = true }
      assert called
    end

    test "allows setting a custom around_request" do
      config = Configuration.new
      call_log = []
      config.around_request = ->(_data, &request_handler) {
        call_log << :before
        request_handler.call
        call_log << :after
      }

      config.around_request.call({}) { call_log << :execute }
      assert_equal([:before, :execute, :after], call_log)
    end

    test "around_request? returns false by default" do
      config = Configuration.new
      refute config.around_request?
    end

    test "around_request? returns true when set" do
      config = Configuration.new
      config.around_request = ->(_data, &request_handler) { request_handler.call }
      assert config.around_request?
    end

    test "merge preserves around_request from other config" do
      custom = ->(_data, &request_handler) { request_handler.call }
      config1 = Configuration.new
      config2 = Configuration.new(around_request: custom)
      merged = config1.merge(config2)
      assert_equal custom, merged.around_request
    end

    test "merge preserves around_request from self when other not set" do
      custom = ->(_data, &request_handler) { request_handler.call }
      config1 = Configuration.new(around_request: custom)
      config2 = Configuration.new
      merged = config1.merge(config2)
      assert_equal custom, merged.around_request
    end

    test "raises ArgumentError when protocol_version is not a supported value" do
      exception = assert_raises(ArgumentError) do
        Configuration.new(protocol_version: "1999-12-31")
      end
      assert_match(/\Aprotocol_version must be/, exception.message)
    end

    test "raises ArgumentError when validate_tool_call_arguments is not a boolean" do
      exception = assert_raises(ArgumentError) do
        Configuration.new(validate_tool_call_arguments: "true")
      end
      assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
    end

    test "raises ArgumentError when validate_tool_call_results is not a boolean" do
      exception = assert_raises(ArgumentError) do
        Configuration.new(validate_tool_call_results: "true")
      end
      assert_equal("validate_tool_call_results must be a boolean", exception.message)
    end
  end
end
