# frozen_string_literal: true

RSpec.describe DiscourseAi::Completions::ToolDefinition do
  # Test case 1: Basic tool definition creation
  describe "#initialize" do
    it "creates a tool with name, description and parameters" do
      param =
        DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
          name: "query",
          description: "The search query",
          type: :string,
          required: true,
        )

      tool =
        described_class.new(
          name: "search_engine",
          description: "Searches the web for information",
          parameters: [param],
        )

      expect(tool.name).to eq("search_engine")
      expect(tool.description).to eq("Searches the web for information")
      expect(tool.parameters.size).to eq(1)
      expect(tool.parameters.first.name).to eq("query")
    end
  end

  # Test case 2: Creating tool from hash
  describe ".from_hash" do
    it "creates a tool from a hash representation" do
      hash = {
        name: "calculator",
        description: "Performs math operations",
        parameters: [
          {
            name: "expression",
            description: "Math expression to evaluate",
            type: "string",
            required: true,
          },
        ],
      }

      tool = described_class.from_hash(hash)

      expect(tool.name).to eq("calculator")
      expect(tool.description).to eq("Performs math operations")
      expect(tool.parameters.size).to eq(1)
      expect(tool.parameters.first.name).to eq("expression")
      expect(tool.parameters.first.type).to eq(:string)
    end

    it "rejects a hash with extra keys" do
      hash = {
        name: "calculator",
        description: "Performs math operations",
        parameters: [],
        extra_key: "should not be here",
      }

      expect { described_class.from_hash(hash) }.to raise_error(ArgumentError, /Unexpected keys/)
    end
  end

  # Test case 3: Parameter with enum validation
  describe DiscourseAi::Completions::ToolDefinition::ParameterDefinition do
    context "with enum values" do
      it "accepts valid enum values matching the type" do
        param =
          described_class.new(
            name: "operation",
            description: "Math operation to perform",
            type: :string,
            enum: %w[add subtract multiply divide],
          )

        expect(param.enum).to eq(%w[add subtract multiply divide])
      end

      it "rejects enum values that don't match the specified type" do
        expect {
          described_class.new(
            name: "operation",
            description: "Math operation to perform",
            type: :integer,
            enum: %w[add subtract], # String values for integer type
          )
        }.to raise_error(ArgumentError, /enum values must be integers/)
      end
    end

    context "with item_type specification" do
      it "only allows item_type for array type parameters" do
        expect {
          described_class.new(
            name: "colors",
            description: "List of colors",
            type: :array,
            item_type: :string,
          )
        }.not_to raise_error

        expect {
          described_class.new(
            name: "color",
            description: "A single color",
            type: :string,
            item_type: :string,
          )
        }.to raise_error(ArgumentError, /item_type can only be specified for array type/)
      end
    end
  end

  # Test case 4: Coercing string parameters
  describe "#coerce_parameters" do
    context "with string parameters" do
      let(:tool) do
        param =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "name",
            description: "User's name",
            type: :string,
          )

        described_class.new(
          name: "greeting",
          description: "Generates a greeting",
          parameters: [param],
        )
      end

      it "converts numbers to strings" do
        result = tool.coerce_parameters(name: 123)
        expect(result[:name]).to eq("123")
      end

      it "converts booleans to strings" do
        result = tool.coerce_parameters(name: true)
        expect(result[:name]).to eq("true")
      end
    end

    # Test case 5: Coercing number parameters
    context "with number parameters" do
      let(:tool) do
        param =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "price",
            description: "Item price",
            type: :number,
          )

        described_class.new(name: "store", description: "Store operations", parameters: [param])
      end

      it "converts string numbers to floats" do
        result = tool.coerce_parameters(price: "42.99")
        expect(result[:price]).to eq(42.99)
      end

      it "converts integers to floats" do
        result = tool.coerce_parameters(price: 42)
        expect(result[:price]).to eq(42.0)
      end

      it "returns nil for invalid number strings" do
        result = tool.coerce_parameters(price: "not a number")
        expect(result[:price]).to be_nil
      end
    end

    # Test case 6: Coercing array parameters with item types
    context "with array parameters and item types" do
      let(:tool) do
        param =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "numbers",
            description: "List of numeric values",
            type: :array,
            item_type: :integer,
          )

        described_class.new(
          name: "stats",
          description: "Statistical operations",
          parameters: [param],
        )
      end

      it "converts string elements to integers" do
        result = tool.coerce_parameters(numbers: %w[1 2 3])
        expect(result[:numbers]).to eq([1, 2, 3])
      end

      it "parses JSON strings into arrays and converts elements" do
        result = tool.coerce_parameters(numbers: "[1, 2, 3]")
        expect(result[:numbers]).to eq([1, 2, 3])
      end

      it "handles mixed type arrays appropriately" do
        result = tool.coerce_parameters(numbers: [1, "two", 3.5])
        expect(result[:numbers]).to eq([1, nil, 3])
      end
    end

    # Test case 7: Required parameters
    context "with required and optional parameters" do
      let(:tool) do
        param1 =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "required_param",
            description: "This is required",
            type: :string,
            required: true,
          )

        param2 =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "optional_param",
            description: "This is optional",
            type: :string,
          )

        described_class.new(
          name: "test_tool",
          description: "Test tool",
          parameters: [param1, param2],
        )
      end

      it "includes missing required parameters as nil" do
        result = tool.coerce_parameters(optional_param: "value")
        expect(result[:required_param]).to be_nil
        expect(result[:optional_param]).to eq("value")
      end

      it "skips missing optional parameters" do
        result = tool.coerce_parameters({})
        expect(result[:required_param]).to be_nil
        expect(result.key?("optional_param")).to be false
      end
    end

    # Test case 8: Boolean parameter coercion
    context "with boolean parameters" do
      let(:tool) do
        param =
          DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
            name: "flag",
            description: "Boolean flag",
            type: :boolean,
          )

        described_class.new(name: "feature", description: "Feature toggle", parameters: [param])
      end

      it "preserves true/false values" do
        result = tool.coerce_parameters(flag: true)
        expect(result[:flag]).to be true
      end

      it "converts 'true'/'false' strings to booleans" do
        result = tool.coerce_parameters({ flag: true })
        expect(result[:flag]).to be true

        result = tool.coerce_parameters({ flag: "False" })
        expect(result[:flag]).to be false
      end

      it "returns nil for invalid boolean strings" do
        result = tool.coerce_parameters({ "flag" => "not a boolean" })
        expect(result["flag"]).to be_nil
      end
    end
  end

  # Test case 9: Duplicate parameter validation
  describe "duplicate parameter validation" do
    it "rejects tool definitions with duplicate parameter names" do
      param1 =
        DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
          name: "query",
          description: "Search query",
          type: :string,
        )

      param2 =
        DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
          name: "query", # Same name as param1
          description: "Another parameter",
          type: :string,
        )

      expect {
        described_class.new(
          name: "search",
          description: "Search tool",
          parameters: [param1, param2],
        )
      }.to raise_error(ArgumentError, /Duplicate parameter names/)
    end
  end

  # Test case 10: Serialization to hash
  describe "#to_h" do
    it "serializes the tool to a hash with all properties" do
      param =
        DiscourseAi::Completions::ToolDefinition::ParameterDefinition.new(
          name: "colors",
          description: "List of colors",
          type: :array,
          item_type: :string,
          required: true,
        )

      tool =
        described_class.new(
          name: "palette",
          description: "Color palette generator",
          parameters: [param],
        )

      hash = tool.to_h

      expect(hash[:name]).to eq("palette")
      expect(hash[:description]).to eq("Color palette generator")
      expect(hash[:parameters].size).to eq(1)

      param_hash = hash[:parameters].first
      expect(param_hash[:name]).to eq("colors")
      expect(param_hash[:type]).to eq(:array)
      expect(param_hash[:item_type]).to eq(:string)
      expect(param_hash[:required]).to eq(true)
    end
  end
end
