--- Unit tests for http_client.lua

-- Add the parent directory to the Lua path so we can load the modules
local script_path = debug.getinfo(1, "S").source:sub(2)
local plugin_dir = script_path:match("(.*/)")  -- Get directory of this file
plugin_dir = plugin_dir:gsub("spec/$", "")  -- Go up to plugin root

-- Add to package.path
package.path = plugin_dir .. "?.lua;" .. plugin_dir .. "?/init.lua;" .. package.path

-- Mock dependencies before loading the module
_G.kong = {
  log = {
    warn = function() end,
  }
}

_G.ngx = {
  sleep = function() end,
}

-- Mock resty.http
package.loaded["resty.http"] = {
  new = function()
    return {
      set_timeout = function() end,
      request_uri = function() end,
    }
  end
}

local http_client = dofile(plugin_dir .. "http_client.lua")

describe("http_client", function()
  local _ngx_sleep
  local _kong_log_warn
  local sleep_calls
  local log_calls
  
  before_each(function()
    -- Mock ngx.sleep
    _ngx_sleep = ngx.sleep
    sleep_calls = {}
    ngx.sleep = function(seconds)
      table.insert(sleep_calls, seconds)
    end
    
    -- Mock kong.log.warn
    _kong_log_warn = kong.log.warn
    log_calls = {}
    kong.log.warn = function(...)
      table.insert(log_calls, table.concat({...}, " "))
    end
  end)
  
  after_each(function()
    ngx.sleep = _ngx_sleep
    kong.log.warn = _kong_log_warn
  end)
  
  describe("new()", function()
    it("creates instance with default options", function()
      local client = http_client.new()
      assert.is_not_nil(client)
      assert.equals(3, client.max_retries)
      assert.equals(100, client.retry_delay)
      assert.equals(10000, client.timeout)
    end)
    
    it("creates instance with custom options", function()
      local client = http_client.new({
        max_retries = 5,
        retry_delay = 200,
        timeout = 5000,
        retry_on_status = {400, 401},
      })
      assert.equals(5, client.max_retries)
      assert.equals(200, client.retry_delay)
      assert.equals(5000, client.timeout)
      assert.is_true(client.custom_retriable_status[400])
      assert.is_true(client.custom_retriable_status[401])
    end)
  end)
  
  describe("set_timeout()", function()
    it("updates timeout value", function()
      local client = http_client.new()
      client:set_timeout(15000)
      assert.equals(15000, client.timeout)
    end)
  end)
  
  describe("_is_retriable_error()", function()
    it("returns true for retriable status codes", function()
      assert.is_true(http_client._is_retriable_error(nil, 408))
      assert.is_true(http_client._is_retriable_error(nil, 429))
      assert.is_true(http_client._is_retriable_error(nil, 500))
      assert.is_true(http_client._is_retriable_error(nil, 502))
      assert.is_true(http_client._is_retriable_error(nil, 503))
      assert.is_true(http_client._is_retriable_error(nil, 504))
    end)
    
    it("returns false for non-retriable status codes", function()
      assert.is_false(http_client._is_retriable_error(nil, 200))
      assert.is_false(http_client._is_retriable_error(nil, 400))
      assert.is_false(http_client._is_retriable_error(nil, 401))
      assert.is_false(http_client._is_retriable_error(nil, 404))
    end)
    
    it("returns true for network timeout errors", function()
      assert.is_true(http_client._is_retriable_error("timeout", nil))
      assert.is_true(http_client._is_retriable_error("operation timeout", nil))
      assert.is_true(http_client._is_retriable_error("connection timeout", nil))
    end)
    
    it("returns true for connection errors", function()
      assert.is_true(http_client._is_retriable_error("connection refused", nil))
      assert.is_true(http_client._is_retriable_error("connection reset", nil))
      assert.is_true(http_client._is_retriable_error("broken pipe", nil))
    end)
    
    it("returns false for other errors", function()
      assert.is_false(http_client._is_retriable_error("invalid response", nil))
      assert.is_false(http_client._is_retriable_error("parse error", nil))
    end)
  end)
  
  describe("_calculate_backoff()", function()
    it("calculates exponential backoff", function()
      -- Mock math.random to return predictable values
      local _math_random = math.random
      math.random = function(min, max)
        return 0  -- No jitter for testing
      end
      
      assert.equals(100, http_client._calculate_backoff(0, 100))
      assert.equals(200, http_client._calculate_backoff(1, 100))
      assert.equals(400, http_client._calculate_backoff(2, 100))
      assert.equals(800, http_client._calculate_backoff(3, 100))
      
      math.random = _math_random
    end)
    
    it("adds jitter to backoff delay", function()
      -- We can't test exact jitter, but we can verify it's in range
      local delay = http_client._calculate_backoff(1, 100)
      assert.is_true(delay >= 200)  -- At least base delay
      assert.is_true(delay <= 300)  -- At most base delay + jitter
    end)
  end)
  
  describe("is_status_retriable()", function()
    it("returns true for default retriable status codes", function()
      local client = http_client.new()
      assert.is_true(client:is_status_retriable(503))
      assert.is_true(client:is_status_retriable(502))
    end)
    
    it("returns true for custom retriable status codes", function()
      local client = http_client.new({
        retry_on_status = {400, 401},
      })
      assert.is_true(client:is_status_retriable(400))
      assert.is_true(client:is_status_retriable(401))
      -- Default ones still work
      assert.is_true(client:is_status_retriable(503))
    end)
    
    it("returns false for non-retriable status codes", function()
      local client = http_client.new()
      assert.is_false(client:is_status_retriable(200))
      assert.is_false(client:is_status_retriable(404))
    end)
  end)
  
  describe("request_uri()", function()
    local _http_new
    local mock_httpc
    
    before_each(function()
      _http_new = require("resty.http").new
      
      -- Mock resty.http
      mock_httpc = {
        set_timeout = function() end,
        request_uri = function(self, uri, params)
          return nil, "mock not implemented"
        end,
      }
      
      require("resty.http").new = function()
        return mock_httpc
      end
    end)
    
    after_each(function()
      require("resty.http").new = _http_new
    end)
    
    it("returns successful response without retry", function()
      mock_httpc.request_uri = function(self, uri, params)
        return {
          status = 200,
          body = "success",
        }, nil
      end
      
      local client = http_client.new()
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      assert.is_nil(err)
      assert.equals(200, res.status)
      assert.equals("success", res.body)
      assert.equals(0, #sleep_calls)  -- No retries
    end)
    
    it("retries on retriable status code", function()
      local attempt = 0
      mock_httpc.request_uri = function(self, uri, params)
        attempt = attempt + 1
        if attempt < 3 then
          return {
            status = 503,
            body = "service unavailable",
          }, nil
        else
          return {
            status = 200,
            body = "success",
          }, nil
        end
      end
      
      local client = http_client.new({
        max_retries = 3,
        retry_delay = 100,
      })
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      assert.is_nil(err)
      assert.equals(200, res.status)
      assert.equals(3, attempt)
      assert.equals(2, #sleep_calls)  -- 2 retries
      assert.equals(2, #log_calls)     -- 2 warning logs
    end)
    
    it("retries on network error", function()
      local attempt = 0
      mock_httpc.request_uri = function(self, uri, params)
        attempt = attempt + 1
        if attempt < 2 then
          return nil, "connection timeout"
        else
          return {
            status = 200,
            body = "success",
          }, nil
        end
      end
      
      local client = http_client.new({
        max_retries = 2,
        retry_delay = 50,
      })
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      assert.is_nil(err)
      assert.equals(200, res.status)
      assert.equals(2, attempt)
      assert.equals(1, #sleep_calls)
    end)
    
    it("stops retrying after max_retries", function()
      mock_httpc.request_uri = function(self, uri, params)
        return {
          status = 503,
          body = "service unavailable",
        }, nil
      end
      
      local client = http_client.new({
        max_retries = 2,
        retry_delay = 50,
      })
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      -- Should return the last error response
      assert.is_nil(err)
      assert.equals(503, res.status)
      assert.equals(2, #sleep_calls)  -- max_retries (2) attempts
    end)
    
    it("does not retry on non-retriable status code", function()
      mock_httpc.request_uri = function(self, uri, params)
        return {
          status = 404,
          body = "not found",
        }, nil
      end
      
      local client = http_client.new()
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      assert.is_nil(err)
      assert.equals(404, res.status)
      assert.equals(0, #sleep_calls)  -- No retries
    end)
    
    it("does not retry on non-retriable network error", function()
      mock_httpc.request_uri = function(self, uri, params)
        return nil, "parse error"
      end
      
      local client = http_client.new()
      local res, err = client:request_uri("http://example.com", {method = "GET"})
      
      assert.is_nil(res)
      assert.equals("parse error", err)
      assert.equals(0, #sleep_calls)  -- No retries
    end)
  end)
end)
