--- Unit tests for authn.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,
  }
}

-- Mock http_client module
local mock_http_client = {
  request_uri_results = {},
  request_uri_calls = {},
}

package.loaded["kong.plugins.o11y-proxy.http_client"] = {
  new = function(opts)
    return {
      options = opts,
      request_uri = function(self, url, params)
        table.insert(mock_http_client.request_uri_calls, {
          url = url,
          params = params,
        })
        
        if #mock_http_client.request_uri_results > 0 then
          local result = table.remove(mock_http_client.request_uri_results, 1)
          return result.response, result.error
        end
        
        -- Default success response
        return {
          status = 200,
          body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
        }, nil
      end,
    }
  end
}

-- Mock cjson
package.loaded["cjson.safe"] = {
  encode = function(t)
    if t.auth0_token == "invalid_json" then
      return nil, "encode error"
    end
    return '{"auth0_token":"' .. (t.auth0_token or "") .. '"}', nil
  end,
  decode = function(str)
    if str:find('"check_result":true') then
      return {
        check_result = true,
        check_message = "",
        user_info = { id = 12345 }
      }, nil
    elseif str:find('"check_result":false') then
      return {
        check_result = false,
        check_message = "Invalid token",
        user_info = nil
      }, nil
    elseif str:find("invalid_decode") then
      return nil, "decode error"
    end
    return {}, nil
  end
}

-- Mock lrucache
local mock_cache = {}
package.loaded["resty.lrucache"] = {
  new = function(max_size)
    return {
      get = function(self, key)
        return mock_cache[key]
      end,
      set = function(self, key, value, ttl)
        mock_cache[key] = value
      end,
    }
  end
}

-- Mock resty.lock
local mock_lock = {
  locked = false,
  lock_calls = {},
  unlock_calls = {},
}
package.loaded["resty.lock"] = {
  new = function(dict_name, opts)
    return {
      lock = function(self, key)
        table.insert(mock_lock.lock_calls, key)
        if mock_lock.locked then
          return nil, "locked"
        end
        mock_lock.locked = true
        return 0.001, nil
      end,
      unlock = function(self)
        table.insert(mock_lock.unlock_calls, true)
        mock_lock.locked = false
        return true
      end,
    }
  end
}

-- Mock resty.md5
package.loaded["resty.md5"] = {
  new = function()
    local data = ""
    return {
      update = function(self, str)
        data = data .. str
      end,
      final = function(self)
        -- Simple hash simulation
        local hash = 0
        for i = 1, #data do
          hash = hash + string.byte(data, i)
        end
        return string.format("%032d", hash)
      end,
    }
  end
}

-- Mock resty.string
package.loaded["resty.string"] = {
  to_hex = function(str)
    return str -- Just return the string as-is for testing
  end
}

-- Load the module under test
local authn = dofile(plugin_dir .. "authn.lua")

describe("authn", function()
  before_each(function()
    -- Clear cache
    mock_cache = {}
    
    -- Clear mock data
    mock_http_client.request_uri_results = {}
    mock_http_client.request_uri_calls = {}
    mock_lock.locked = false
    mock_lock.lock_calls = {}
    mock_lock.unlock_calls = {}
  end)
  
  describe("authenticate()", function()
    it("returns error when authn_url is not configured", function()
      local result, err = authn.authenticate("", "token123")
      assert.is_nil(result)
      assert.equals("Authn service URL is not configured", err)
    end)
    
    it("returns error when auth0_token is empty", function()
      local result, err = authn.authenticate("http://authn-svc/api/authn", "")
      assert.is_nil(result)
      assert.equals("Auth0 token is required", err)
    end)
    
    it("successfully authenticates with valid token", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "valid_token",
        10000,
        1800
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      assert.is_true(result.check_result)
      assert.equals(12345, result.user_id)
      
      -- Verify HTTP client was called correctly
      assert.equals(1, #mock_http_client.request_uri_calls)
      local call = mock_http_client.request_uri_calls[1]
      assert.equals("http://authn-svc/api/authn", call.url)
      assert.equals("POST", call.params.method)
      assert.equals("application/json", call.params.headers["Content-Type"])
    end)
    
    it("handles authentication failure with false check_result", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":false,"check_message":"Invalid token","user_info":null}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "invalid_token",
        10000,
        1800
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      assert.is_false(result.check_result)
      assert.is_nil(result.user_id)
    end)
    
    it("returns error when HTTP request fails", function()
      mock_http_client.request_uri_results = {
        {
          response = nil,
          error = "connection timeout"
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "token123",
        10000,
        1800
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to query authn service") ~= nil)
    end)
    
    it("returns error when service returns non-200 status", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 500,
            body = "Internal Server Error"
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "token123",
        10000,
        1800
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Authn service returned error status: 500") ~= nil)
    end)
    
    it("returns error when JSON encoding fails", function()
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "invalid_json",
        10000,
        1800
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to encode authn request") ~= nil)
    end)
    
    it("returns error when JSON decoding fails", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"invalid_decode":true}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "token123",
        10000,
        1800
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to decode authn response") ~= nil)
    end)
    
    it("returns error when response is missing check_result field", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "token123",
        10000,
        1800
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Authn service response missing check_result field") ~= nil)
    end)
    
    it("uses cache for repeated requests with same token", function()
      -- First request - cache miss
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authn.authenticate(
        "http://authn-svc/api/authn",
        "cached_token",
        10000,
        1800
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.is_true(result1.check_result)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request - cache hit (no HTTP call)
      local result2, err2 = authn.authenticate(
        "http://authn-svc/api/authn",
        "cached_token",
        10000,
        1800
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      assert.is_true(result2.check_result)
      assert.equals(12345, result2.user_id)
      -- HTTP client should not be called again
      assert.equals(1, #mock_http_client.request_uri_calls)
    end)
    
    it("skips cache when cache_ttl is 0", function()
      -- First request
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        },
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authn.authenticate(
        "http://authn-svc/api/authn",
        "no_cache_token",
        10000,
        0  -- cache_ttl = 0, disable cache
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request - should make another HTTP call
      local result2, err2 = authn.authenticate(
        "http://authn-svc/api/authn",
        "no_cache_token",
        10000,
        0  -- cache_ttl = 0, disable cache
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      -- HTTP client should be called again
      assert.equals(2, #mock_http_client.request_uri_calls)
    end)
    
    it("uses default timeout when not specified", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "token123"
        -- timeout not specified
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      -- Verify timeout was passed to http_client (10000 is default)
      assert.equals(1, #mock_http_client.request_uri_calls)
    end)
    
    it("caches both successful and failed authentication results", function()
      -- First request - authentication failure
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":false,"check_message":"Invalid token","user_info":null}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authn.authenticate(
        "http://authn-svc/api/authn",
        "failed_token",
        10000,
        1800
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.is_false(result1.check_result)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request - should use cached failure result
      local result2, err2 = authn.authenticate(
        "http://authn-svc/api/authn",
        "failed_token",
        10000,
        1800
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      assert.is_false(result2.check_result)
      -- HTTP client should not be called again
      assert.equals(1, #mock_http_client.request_uri_calls)
    end)
    
    it("acquires lock for concurrent requests with same token", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"check_result":true,"check_message":"","user_info":{"id":12345}}'
          },
          error = nil
        }
      }
      
      local result, err = authn.authenticate(
        "http://authn-svc/api/authn",
        "lock_token",
        10000,
        1800
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      
      -- Verify lock was acquired and released
      assert.equals(1, #mock_lock.lock_calls)
      assert.equals(1, #mock_lock.unlock_calls)
    end)
  end)
end)
