--- Unit tests for aws_signer.lua
-- Tests the main sign_request function

-- 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 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 loading the module
_G.ngx = {
  time = function()
    -- Fixed timestamp for consistent testing: 2024-01-01 00:00:00 UTC
    return 1704067200
  end,
  unescape_uri = function(str)
    -- Simple implementation for testing
    return str:gsub("%%(%x%x)", function(h)
      return string.char(tonumber(h, 16))
    end)
  end,
  escape_uri = function(str)
    -- Simple implementation for testing
    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 a fake hash for testing
        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", function()
  
  describe("AwsSigner.sign_request", function()
    local test_credentials = {
      access_key_id = "AKIAIOSFODNN7EXAMPLE",
      secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    }
    
    it("should sign a basic POST request", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        path = "/",
        credentials = test_credentials,
        body = "Action=AssumeRole&Version=2011-06-15",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.is_not_nil(headers["Authorization"])
      assert.is_not_nil(headers["X-Amz-Date"])
      assert.equals("sts.amazonaws.com", headers["Host"])
      assert.equals("application/x-www-form-urlencoded", headers["Content-Type"])
      assert.is_true(headers["Authorization"]:match("AWS4%-HMAC%-SHA256") ~= nil)
    end)
    
    it("should sign a GET request without Content-Type", function()
      local opts = {
        method = "GET",
        host = "s3.amazonaws.com",
        path = "/bucket/key",
        credentials = test_credentials,
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.is_not_nil(headers["Authorization"])
      assert.is_not_nil(headers["X-Amz-Date"])
      assert.is_nil(headers["Content-Type"])  -- GET should not have Content-Type
    end)
    
    it("should include session token when provided", function()
      local creds_with_token = {
        access_key_id = "AKIAIOSFODNN7EXAMPLE",
        secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        session_token = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE",
      }
      
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        path = "/",
        credentials = creds_with_token,
        body = "Action=AssumeRole",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.equals(creds_with_token.session_token, headers["X-Amz-Security-Token"])
    end)
    
    it("should handle custom region and service", function()
      local opts = {
        method = "POST",
        host = "sts.us-west-2.amazonaws.com",
        path = "/",
        credentials = test_credentials,
        region = "us-west-2",
        service = "sts",
        body = "",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.is_not_nil(headers["Authorization"])
      -- Check that region is included in the credential scope
      assert.is_true(headers["Authorization"]:match("us%-west%-2/sts/aws4_request") ~= nil)
    end)
    
    it("should handle query string", function()
      local opts = {
        method = "GET",
        host = "s3.amazonaws.com",
        path = "/",
        credentials = test_credentials,
        query_string = "prefix=test&max-keys=100",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.is_not_nil(headers["Authorization"])
    end)
    
    it("should return error when opts is nil", function()
      local headers, err = aws_signer.sign_request(nil)
      
      assert.is_nil(headers)
      assert.equals("Options table is required", err)
    end)
    
    it("should return error when method is missing", function()
      local opts = {
        host = "sts.amazonaws.com",
        credentials = test_credentials,
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("HTTP method is required", err)
    end)
    
    it("should return error when method is empty", function()
      local opts = {
        method = "",
        host = "sts.amazonaws.com",
        credentials = test_credentials,
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("HTTP method is required", err)
    end)
    
    it("should return error when host is missing", function()
      local opts = {
        method = "POST",
        credentials = test_credentials,
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("Host is required", err)
    end)
    
    it("should return error when host is empty", function()
      local opts = {
        method = "POST",
        host = "",
        credentials = test_credentials,
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("Host is required", err)
    end)
    
    it("should return error when credentials are missing", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("Valid credentials with access_key_id and secret_access_key are required", err)
    end)
    
    it("should return error when access_key_id is missing", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        credentials = {
          secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        },
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("Valid credentials with access_key_id and secret_access_key are required", err)
    end)
    
    it("should return error when secret_access_key is missing", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        credentials = {
          access_key_id = "AKIAIOSFODNN7EXAMPLE",
        },
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(headers)
      assert.equals("Valid credentials with access_key_id and secret_access_key are required", err)
    end)
    
    it("should strip standard port from host", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com:443",
        path = "/",
        credentials = test_credentials,
        body = "",
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.equals("sts.amazonaws.com", headers["Host"])  -- Port should be stripped
    end)
    
    it("should handle custom headers", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        path = "/",
        credentials = test_credentials,
        headers = {
          ["Content-Type"] = "application/json",
        },
        body = '{"key":"value"}',
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.equals("application/json", headers["Content-Type"])
    end)
    
    it("should use default values for optional parameters", function()
      local opts = {
        method = "POST",
        host = "sts.amazonaws.com",
        credentials = test_credentials,
        -- path, headers, body, region, service, query_string all omitted
      }
      
      local headers, err = aws_signer.sign_request(opts)
      
      assert.is_nil(err)
      assert.is_not_nil(headers)
      assert.is_not_nil(headers["Authorization"])
      -- Should use default region us-east-1 and service sts
      assert.is_true(headers["Authorization"]:match("us%-east%-1/sts/aws4_request") ~= nil)
    end)
  end)
end)
