--- Unit tests for pure functions in aws_signer.lua
-- These tests focus on internal functions without external dependencies

-- Add the parent directory to the Lua path
local script_path = debug.getinfo(1, "S").source:sub(2)
local plugin_dir = script_path:match("(.*/)")
plugin_dir = plugin_dir:gsub("spec/$", "")

-- Mock FFI (LuaJIT FFI) before loading the module
local ffi_mock = {
  cdef = function() end,
  new = function(ctype, ...)
    if ctype == "unsigned char[32]" then
      return {0}  -- Return a mock buffer
    elseif ctype == "unsigned int[1]" then
      return {32}  -- Return mock md_len
    end
    return {}
  end,
  cast = function(ctype, data)
    return data
  end,
  string = function(ptr, len)
    return "mocked_hmac_result"
  end,
  C = {
    EVP_sha256 = function() return "mock_evp_md" end,
    HMAC = function(evp_md, key, key_len, data, data_len, md, md_len)
      md_len[0] = 32
      return md  -- Return non-nil to indicate success
    end,
  }
}
package.loaded["ffi"] = ffi_mock

-- Mock dependencies before requiring the module
_G.ngx = {
  time = function()
    return 1704067200  -- Fixed timestamp: 2024-01-01 00:00:00 UTC
  end,
  unescape_uri = function(str)
    -- URL decode implementation
    return str:gsub("%%(%x%x)", function(h)
      return string.char(tonumber(h, 16))
    end)
  end,
  escape_uri = function(str)
    -- URL encode implementation
    return str:gsub("[^%w%-%.%_%~]", function(c)
      return string.format("%%%02X", string.byte(c))
    end)
  end
}

-- Mock resty.sha256
package.loaded["resty.sha256"] = {
  new = function()
    return {
      update = function(self, data)
        self.data = (self.data or "") .. data
      end,
      final = function(self)
        return "fake_hash_" .. #(self.data or "")
      end
    }
  end
}

-- Mock resty.string
package.loaded["resty.string"] = {
  to_hex = function(str)
    return str:gsub(".", function(c)
      return string.format("%02x", string.byte(c))
    end)
  end
}

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

describe("aws_signer.lua - pure functions", function()
  
  describe("canonicalize_path", function()
    local canonicalize_path = aws_signer._canonicalize_path
    
    it("should return '/' for nil path", function()
      assert.equals("/", canonicalize_path(nil))
    end)
    
    it("should return '/' for empty path", function()
      assert.equals("/", canonicalize_path(""))
    end)
    
    it("should return '/' for root path", function()
      assert.equals("/", canonicalize_path("/"))
    end)
    
    it("should canonicalize simple path", function()
      assert.equals("/bucket/key", canonicalize_path("/bucket/key"))
    end)
    
    it("should preserve trailing slash", function()
      assert.equals("/bucket/", canonicalize_path("/bucket/"))
    end)
    
    it("should not add trailing slash to root", function()
      assert.equals("/", canonicalize_path("/"))
    end)
    
    it("should handle path with '..' segments", function()
      assert.equals("/bucket/key", canonicalize_path("/bucket/folder/../key"))
    end)
    
    it("should handle path with '.' segments", function()
      assert.equals("/bucket/key", canonicalize_path("/bucket/./key"))
    end)
    
    it("should handle multiple '..' segments", function()
      assert.equals("/key", canonicalize_path("/bucket/folder/../../key"))
    end)
    
    it("should handle '..' at the beginning", function()
      assert.equals("/bucket", canonicalize_path("/../bucket"))
    end)
    
    it("should handle empty segments", function()
      assert.equals("/bucket/key", canonicalize_path("/bucket//key"))
    end)
    
    it("should handle complex path", function()
      assert.equals("/bucket/key", canonicalize_path("/bucket/./folder/../key"))
    end)
    
    it("should URL encode special characters", function()
      local result = canonicalize_path("/bucket/key with spaces")
      assert.is_true(result:match("%%20") ~= nil)  -- Spaces should be encoded
    end)
    
    it("should preserve safe characters", function()
      local result = canonicalize_path("/bucket/key-_.~")
      assert.is_true(result:match("%-_%.~") ~= nil)
    end)
    
    it("should handle path ending with '..'", function()
      assert.equals("/", canonicalize_path("/bucket/.."))
    end)
    
    it("should handle path ending with '.'", function()
      assert.equals("/bucket", canonicalize_path("/bucket/."))
    end)
  end)
  
  describe("canonicalize_query_string", function()
    local canonicalize_query_string = aws_signer._canonicalize_query_string
    
    it("should return empty string for nil query_string", function()
      assert.equals("", canonicalize_query_string(nil))
    end)
    
    it("should return empty string for empty query_string", function()
      assert.equals("", canonicalize_query_string(""))
    end)
    
    it("should sort parameters alphabetically", function()
      local result = canonicalize_query_string("z=1&a=2&m=3")
      assert.equals("a=2&m=3&z=1", result)
    end)
    
    it("should handle single parameter", function()
      assert.equals("key=value", canonicalize_query_string("key=value"))
    end)
    
    it("should handle parameter without value", function()
      assert.equals("key=", canonicalize_query_string("key="))
    end)
    
    it("should handle parameter with empty value", function()
      assert.equals("key=", canonicalize_query_string("key="))
    end)
    
    it("should URL encode parameter names and values", function()
      local result = canonicalize_query_string("key name=value space")
      -- Should be URL encoded
      assert.is_true(result:match("%%20") ~= nil)
    end)
    
    it("should handle multiple parameters with same name", function()
      local result = canonicalize_query_string("key=1&key=2")
      -- Both should appear, sorted
      assert.is_true(result:match("key=1") ~= nil)
      assert.is_true(result:match("key=2") ~= nil)
    end)
    
    it("should preserve safe characters in parameters", function()
      local result = canonicalize_query_string("key-_.~=value-_.~")
      assert.is_true(result:match("%-_%.~") ~= nil)
    end)
    
    it("should handle already encoded parameters", function()
      local result = canonicalize_query_string("key=%20value")
      -- Should decode and re-encode
      assert.is_not_nil(result)
    end)
    
    it("should handle special AWS characters", function()
      local result = canonicalize_query_string("Action=AssumeRole&Version=2011-06-15")
      assert.is_true(result:match("Action=") ~= nil)
      assert.is_true(result:match("Version=") ~= nil)
    end)
    
    it("should handle complex query string", function()
      local result = canonicalize_query_string("prefix=test&max-keys=100&delimiter=/")
      assert.is_not_nil(result)
      -- Should be sorted
      assert.is_true(result:find("delimiter=") < result:find("max%-keys="))
      assert.is_true(result:find("max%-keys=") < result:find("prefix="))
    end)
  end)
  
  describe("build_canonical_headers", function()
    local build_canonical_headers = aws_signer._build_canonical_headers
    
    it("should build basic canonical headers for POST", function()
      local headers_str, signed_headers = build_canonical_headers(
        "sts.amazonaws.com",
        "POST",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_not_nil(headers_str)
      assert.is_not_nil(signed_headers)
      assert.is_true(headers_str:match("host:sts%.amazonaws%.com\n") ~= nil)
      assert.is_true(headers_str:match("x%-amz%-date:20240101T000000Z\n") ~= nil)
      assert.is_true(headers_str:match("content%-type:application/x%-www%-form%-urlencoded\n") ~= nil)
      assert.equals("content-type;host;x-amz-date", signed_headers)
    end)
    
    it("should build canonical headers for GET without content-type", function()
      local headers_str, signed_headers = build_canonical_headers(
        "s3.amazonaws.com",
        "GET",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_not_nil(headers_str)
      assert.is_not_nil(signed_headers)
      assert.is_true(headers_str:match("host:s3%.amazonaws%.com\n") ~= nil)
      assert.is_true(headers_str:match("x%-amz%-date:20240101T000000Z\n") ~= nil)
      assert.is_false(headers_str:match("content%-type") ~= nil)
      assert.equals("host;x-amz-date", signed_headers)
    end)
    
    it("should include session token when provided", function()
      local headers_str, signed_headers = build_canonical_headers(
        "sts.amazonaws.com",
        "POST",
        {},
        "20240101T000000Z",
        "test-session-token"
      )
      
      assert.is_not_nil(headers_str)
      assert.is_true(headers_str:match("x%-amz%-security%-token:test%-session%-token\n") ~= nil)
      assert.is_true(signed_headers:match("x%-amz%-security%-token") ~= nil)
    end)
    
    it("should remove standard port 443 from host", function()
      local headers_str, signed_headers = build_canonical_headers(
        "sts.amazonaws.com:443",
        "POST",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("host:sts%.amazonaws%.com\n") ~= nil)
      assert.is_false(headers_str:match(":443") ~= nil)
    end)
    
    it("should remove standard port 80 from host", function()
      local headers_str, signed_headers = build_canonical_headers(
        "example.com:80",
        "POST",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("host:example%.com\n") ~= nil)
      assert.is_false(headers_str:match(":80") ~= nil)
    end)
    
    it("should preserve non-standard port", function()
      local headers_str, signed_headers = build_canonical_headers(
        "example.com:8080",
        "POST",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("host:example%.com:8080\n") ~= nil)
    end)
    
    it("should use custom Content-Type when provided", function()
      local headers_str, signed_headers = build_canonical_headers(
        "sts.amazonaws.com",
        "POST",
        {["Content-Type"] = "application/json"},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("content%-type:application/json\n") ~= nil)
    end)
    
    it("should include Content-Type for GET if explicitly provided", function()
      local headers_str, signed_headers = build_canonical_headers(
        "s3.amazonaws.com",
        "GET",
        {["Content-Type"] = "application/json"},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("content%-type:application/json\n") ~= nil)
      assert.is_true(signed_headers:match("content%-type") ~= nil)
    end)
    
    it("should trim whitespace from header values", function()
      local headers_str, signed_headers = build_canonical_headers(
        "  sts.amazonaws.com  ",
        "POST",
        {},
        "  20240101T000000Z  ",
        nil
      )
      
      assert.is_not_nil(headers_str)
      -- Header values should be trimmed (no leading/trailing spaces)
      -- The host input has spaces but after port removal and trim, they should be gone
      assert.is_true(headers_str:match("host:sts%.amazonaws%.com\n") ~= nil)
      assert.is_true(headers_str:match("x%-amz%-date:20240101T000000Z\n") ~= nil)
    end)
    
    it("should handle DELETE method without content-type", function()
      local headers_str, signed_headers = build_canonical_headers(
        "s3.amazonaws.com",
        "DELETE",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_false(headers_str:match("content%-type") ~= nil)
      assert.equals("host;x-amz-date", signed_headers)
    end)
    
    it("should handle HEAD method without content-type", function()
      local headers_str, signed_headers = build_canonical_headers(
        "s3.amazonaws.com",
        "HEAD",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_false(headers_str:match("content%-type") ~= nil)
      assert.equals("host;x-amz-date", signed_headers)
    end)
    
    it("should handle PUT method with content-type", function()
      local headers_str, signed_headers = build_canonical_headers(
        "s3.amazonaws.com",
        "PUT",
        {},
        "20240101T000000Z",
        nil
      )
      
      assert.is_true(headers_str:match("content%-type") ~= nil)
      assert.is_true(signed_headers:match("content%-type") ~= nil)
    end)
    
    it("should sort signed headers alphabetically", function()
      local headers_str, signed_headers = build_canonical_headers(
        "sts.amazonaws.com",
        "POST",
        {},
        "20240101T000000Z",
        "test-token"
      )
      
      -- Should be: content-type, host, x-amz-date, x-amz-security-token
      assert.equals("content-type;host;x-amz-date;x-amz-security-token", signed_headers)
    end)
  end)
end)
