--- Unit tests for authz.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 = '{"has_permission":true,"message":""}'
        }, nil
      end,
    }
  end
}

-- Mock cjson
package.loaded["cjson.safe"] = {
  encode = function(t)
    if t.user_id == -1 then
      return nil, "encode error"
    end
    return '{"user_id":' .. tostring(t.user_id) .. ',"resource_uri":"' .. (t.resource_uri or "") .. '","req_uri":"' .. (t.req_uri or "") .. '"}', nil
  end,
  decode = function(str)
    if str:find('"has_permission":true') then
      return {
        has_permission = true,
        message = ""
      }, nil
    elseif str:find('"has_permission":false') then
      return {
        has_permission = false,
        message = "Access denied"
      }, 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 authz = dofile(plugin_dir .. "authz.lua")

describe("authz", 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("check_permission()", function()
    it("returns error when authz_url is not configured", function()
      local result, err = authz.check_permission("", 12345, "/api/resource", "/api/v1/data")
      assert.is_nil(result)
      assert.equals("Authz service URL is not configured", err)
    end)
    
    it("returns error when user_id is not provided", function()
      local result, err = authz.check_permission("http://authz-svc/api/authz", nil, "/api/resource", "/api/v1/data")
      assert.is_nil(result)
      assert.equals("User ID is required", err)
    end)
    
    it("returns error when resource_uri is empty", function()
      local result, err = authz.check_permission("http://authz-svc/api/authz", 12345, "", "/api/v1/data")
      assert.is_nil(result)
      assert.equals("Resource URI is required", err)
    end)
    
    it("returns error when req_uri is empty", function()
      local result, err = authz.check_permission("http://authz-svc/api/authz", 12345, "/api/resource", "")
      assert.is_nil(result)
      assert.equals("Request URI is required", err)
    end)
    
    it("successfully checks permission with valid parameters", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      assert.is_true(result.has_permission)
      
      -- 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://authz-svc/api/authz", call.url)
      assert.equals("POST", call.params.method)
      assert.equals("application/json", call.params.headers["Content-Type"])
    end)
    
    it("handles permission denial with false has_permission", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":false,"message":"Access denied"}'
          },
          error = nil
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err)
      assert.is_not_nil(result)
      assert.is_false(result.has_permission)
    end)
    
    it("returns error when HTTP request fails", function()
      mock_http_client.request_uri_results = {
        {
          response = nil,
          error = "connection timeout"
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to query authz 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 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Authz service returned error status: 500") ~= nil)
    end)
    
    it("returns error when JSON encoding fails", function()
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        -1,  -- This triggers encode error in mock
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to encode authz 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 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Failed to decode authz response") ~= nil)
    end)
    
    it("returns error when response is missing has_permission field", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{}'
          },
          error = nil
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(result)
      assert.is_not_nil(err)
      assert.is_true(err:find("Authz service response missing has_permission field") ~= nil)
    end)
    
    it("uses cache for repeated requests with same parameters", function()
      -- First request - cache miss
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.is_true(result1.has_permission)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request - cache hit (no HTTP call)
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      assert.is_true(result2.has_permission)
      -- HTTP client should not be called again
      assert.equals(1, #mock_http_client.request_uri_calls)
    end)
    
    it("uses different cache keys for different parameters", function()
      -- First request with user_id=12345
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        },
        {
          response = {
            status = 200,
            body = '{"has_permission":false,"message":"Access denied"}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err1)
      assert.is_true(result1.has_permission)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request with different user_id=67890 - should not use cache
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        67890,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err2)
      assert.is_false(result2.has_permission)
      -- HTTP client should be called again
      assert.equals(2, #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 = '{"has_permission":true,"message":""}'
          },
          error = nil
        },
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        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 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        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 = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data"
        -- 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("uses default cache_ttl of 60 seconds when not specified", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000
        -- cache_ttl not specified, defaults to 60
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request should use cache
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      -- HTTP client should not be called again
      assert.equals(1, #mock_http_client.request_uri_calls)
    end)
    
    it("caches both permission granted and denied results", function()
      -- First request - permission denied
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":false,"message":"Access denied"}'
          },
          error = nil
        }
      }
      
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/forbidden",
        10000,
        60
      )
      
      assert.is_nil(err1)
      assert.is_not_nil(result1)
      assert.is_false(result1.has_permission)
      assert.equals(1, #mock_http_client.request_uri_calls)
      
      -- Second request - should use cached denial result
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/forbidden",
        10000,
        60
      )
      
      assert.is_nil(err2)
      assert.is_not_nil(result2)
      assert.is_false(result2.has_permission)
      -- 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 parameters", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        }
      }
      
      local result, err = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      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)
    
    it("handles different resource URIs correctly", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        },
        {
          response = {
            status = 200,
            body = '{"has_permission":false,"message":"Access denied"}'
          },
          error = nil
        }
      }
      
      -- Request for resource1
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource1",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err1)
      assert.is_true(result1.has_permission)
      
      -- Request for resource2 (different resource_uri)
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource2",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err2)
      assert.is_false(result2.has_permission)
      
      -- Both should make HTTP calls (different cache keys)
      assert.equals(2, #mock_http_client.request_uri_calls)
    end)
    
    it("handles different request URIs correctly", function()
      mock_http_client.request_uri_results = {
        {
          response = {
            status = 200,
            body = '{"has_permission":true,"message":""}'
          },
          error = nil
        },
        {
          response = {
            status = 200,
            body = '{"has_permission":false,"message":"Access denied"}'
          },
          error = nil
        }
      }
      
      -- Request for /api/v1/data
      local result1, err1 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/data",
        10000,
        60
      )
      
      assert.is_nil(err1)
      assert.is_true(result1.has_permission)
      
      -- Request for /api/v1/admin (different req_uri)
      local result2, err2 = authz.check_permission(
        "http://authz-svc/api/authz",
        12345,
        "/api/resource",
        "/api/v1/admin",
        10000,
        60
      )
      
      assert.is_nil(err2)
      assert.is_false(result2.has_permission)
      
      -- Both should make HTTP calls (different cache keys)
      assert.equals(2, #mock_http_client.request_uri_calls)
    end)
  end)
end)
