# frozen_string_literal: true

require 'msf/core/mcp'

RSpec.describe Msf::MCP::Tools::VulnerabilityInfo do
  let(:msf_client) { double('Msf::MCP::Metasploit::Client') }
  let(:rate_limiter) { double('Msf::MCP::Security::RateLimiter') }
  let(:server_context) do
    {
      msf_client: msf_client,
      rate_limiter: rate_limiter,
      config: {}
    }
  end

  let(:msf_response) do
    {
      'vulns' => [
        {
          'host' => '192.168.1.100',
          'port' => 445,
          'proto' => 'tcp',
          'name' => 'MS17-010',
          'refs' => 'CVE-2017-0144,MS17-010',
          'time' => 1609459200
        },
        {
          'host' => '192.168.1.101',
          'port' => 80,
          'proto' => 'tcp',
          'name' => 'Apache Struts RCE',
          'refs' => 'CVE-2017-5638',
          'time' => 1609459300
        }
      ]
    }
  end

  before do
    allow(rate_limiter).to receive(:check_rate_limit!)
    allow(msf_client).to receive(:db_vulns).and_return(msf_response)
  end

  describe 'Tool Name' do
    it 'has the correct tool name' do
      expect(described_class.tool_name).to eq('msf_vulnerability_info')
    end
  end

  describe 'Input Schema Validation' do
    it 'defines workspace as required parameter' do
      input_schema = described_class.input_schema
      expect(input_schema.schema[:required]).to include('workspace')
    end
  end

  describe 'Output Schema' do
    it 'returns vulnerabilities with CVE, severity, exploit_available' do
      data_items = described_class.output_schema.schema[:properties][:data][:items][:properties]

      expect(data_items[:host]).to eq({ type: 'string' })
      expect(data_items[:port]).to eq({ type: 'integer' })
      expect(data_items[:protocol]).to eq({ type: 'string' })
      expect(data_items[:name]).to eq({ type: 'string' })
      expect(data_items[:references]).to eq({ type: 'array', items: { type: 'string' } })
      expect(data_items[:created_at]).to eq({ type: 'string' })
      expect(data_items[:resource]).to eq({ type: 'object' })
    end
  end

  describe '.call' do
    it 'checks rate limit' do
      described_class.call(server_context: server_context)
      expect(rate_limiter).to have_received(:check_rate_limit!).with('vulnerability_info')
    end

    it 'calls Metasploit client with workspace' do
      described_class.call(workspace: 'test_ws', server_context: server_context)
      expect(msf_client).to have_received(:db_vulns).with(hash_including(workspace: 'test_ws'))
    end

    it 'uses default workspace' do
      described_class.call(server_context: server_context)
      expect(msf_client).to have_received(:db_vulns).with(hash_including(workspace: 'default'))
    end

    it 'returns MCP::Tool::Response' do
      result = described_class.call(server_context: server_context)
      expect(result).to be_a(MCP::Tool::Response)
    end

    it 'includes metadata in response' do
      result = described_class.call(workspace: 'default', server_context: server_context)

      metadata = result.structured_content[:metadata]
      expect(metadata[:workspace]).to eq('default')
      expect(metadata[:query_time]).to be_a(Float)
      expect(metadata[:total_items]).to eq(2)
      expect(metadata[:returned_items]).to eq(2)
      expect(metadata[:limit]).to eq(100)
      expect(metadata[:offset]).to eq(0)
    end

    it 'includes transformed data in response' do
      result = described_class.call(server_context: server_context)

      data = result.structured_content[:data]
      expect(data).to be_an(Array)
      expect(data.length).to eq(2)
      expect(data.first[:host]).to eq('192.168.1.100')
      expect(data.first[:name]).to eq('MS17-010')
      expect(data.first[:port]).to eq(445)
      expect(data.first[:protocol]).to eq('tcp')
    end

    it 'handles pagination with limit' do
      result = described_class.call(limit: 1, server_context: server_context)

      metadata = result.structured_content[:metadata]
      expect(metadata[:limit]).to eq(1)
      expect(metadata[:returned_items]).to eq(1)
      expect(result.structured_content[:data].length).to eq(1)
    end

    it 'handles pagination with offset' do
      result = described_class.call(offset: 1, server_context: server_context)

      metadata = result.structured_content[:metadata]
      expect(metadata[:offset]).to eq(1)
      expect(metadata[:returned_items]).to eq(1)
      expect(result.structured_content[:data].length).to eq(1)
    end

    it 'handles filtering by names' do
      described_class.call(names: ['MS17-010'], server_context: server_context)

      # Names array is joined with commas before being sent to API
      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(names: 'MS17-010')
      )
    end

    it 'handles filtering by host' do
      described_class.call(host: '192.168.1.100', server_context: server_context)

      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(address: '192.168.1.100')
      )
    end

    it 'handles filtering by ports' do
      described_class.call(ports: '445', server_context: server_context)

      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(ports: '445')
      )
    end

    it 'handles filtering by protocol' do
      described_class.call(protocol: 'tcp', server_context: server_context)

      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(proto: 'tcp')
      )
    end

    it 'handles empty results' do
      allow(msf_client).to receive(:db_vulns).and_return({ 'vulns' => [] })

      result = described_class.call(server_context: server_context)

      expect(result.structured_content[:data]).to eq([])
      expect(result.structured_content[:metadata][:total_items]).to eq(0)
      expect(result.structured_content[:metadata][:returned_items]).to eq(0)
    end

    it 'returns error response for rate limit exceeded' do
      allow(rate_limiter).to receive(:check_rate_limit!)
        .and_raise(Msf::MCP::Security::RateLimitExceededError.new(60))

      result = described_class.call(server_context: server_context)
      expect(result.error?).to be true
      expect(result.content.first[:text]).to match(/Rate limit exceeded/)
    end

    it 'returns error response for API errors' do
      allow(msf_client).to receive(:db_vulns)
        .and_raise(Msf::MCP::Metasploit::APIError, 'Database error')

      result = described_class.call(server_context: server_context)
      expect(result.error?).to be true
      expect(result.content.first[:text]).to match(/Metasploit API error/)
    end

    it 'returns error response for authentication errors' do
      allow(msf_client).to receive(:db_vulns).and_raise(
        Msf::MCP::Metasploit::AuthenticationError.new('Invalid token')
      )

      result = described_class.call(server_context: server_context)
      expect(result.error?).to be true
      expect(result.content.first[:text]).to match(/Authentication failed/)
    end

    it 'passes pagination parameters to MSF client' do
      # Pagination is applied client-side, not in the API call
      described_class.call(limit: 50, offset: 10, server_context: server_context)

      # API call only receives workspace parameter
      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(workspace: 'default')
      )
    end

    it 'handles multiple name filters' do
      names = ['MS17-010', 'Apache Struts RCE']
      described_class.call(names: names, server_context: server_context)

      # Names array is joined with commas before being sent to API
      expect(msf_client).to have_received(:db_vulns).with(
        hash_including(names: 'MS17-010,Apache Struts RCE')
      )
    end
  end
end
