--- Unit tests for access function in handler.lua

-- 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/$", "")

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

-- Mock dependencies
local mock_kong = {
  log = {
    notice = function() end,
    err = function() end,
    warn = function() end,
  },
  ctx = {
    shared = {},
  },
  request = {
    get_header = function(name) return nil end,
    get_method = function() return "GET" end,
    get_raw_body = function() return "" end,
    get_headers = function() return {} end,
    get_path = function() return "/test/path" end,
    get_forwarded_prefix = function() return nil end,
    get_raw_query = function() return "" end,
  },
  response = {
    exit = function(status, body) error({status = status, body = body}) end, -- Throw error to catch exit
  },
  service = {
    set_target = function() end,
    request = {
      set_scheme = function() end,
      set_path = function() end,
      set_raw_query = function() end,
      set_header = function() end,
    },
  },
}
_G.kong = mock_kong

-- Mock cjson
package.loaded["cjson.safe"] = {
  decode = function(str)
    if str == '{"id": 12345}' then
      return { id = 12345 }
    end
    if str == 'invalid_json' then
      return nil, "Expected value but found invalid token at character 1"
    end
    return nil, "parse error"
  end
}

-- Mock resty.lrucache
package.loaded["resty.lrucache"] = {
  new = function()
    local cache = {}
    return {
      get = function(self, key) return cache[key] end,
      set = function(self, key, value) cache[key] = value end,
      delete = function(self, key) cache[key] = nil end,
      flush_all = function(self) cache = {} end,
    }
  end
}

-- Mock kong.meta
package.loaded["kong.meta"] = {
  version = "1.0.0"
}

-- Mock other modules
package.loaded["kong.plugins.o11y-proxy.assume"] = {
  new = function() return { get_credentials = function() return { access_key_id = "key", secret_access_key = "secret", session_token = "token" } end } end
}

local mock_router = {
  query_target = function() return {
    dns_address = "https://upstream.com",
    access_principal = "",
  }, nil end
}
package.loaded["kong.plugins.o11y-proxy.router"] = mock_router

local mock_authz = {
  check_permission = function() return { has_permission = true }, nil end
}
package.loaded["kong.plugins.o11y-proxy.authz"] = mock_authz

local mock_authn = {
  authenticate = function() return { check_result = true, user_id = 12345 }, nil end
}
package.loaded["kong.plugins.o11y-proxy.authn"] = mock_authn

local captured_project_id_from_mapper = nil
package.loaded["kong.plugins.o11y-proxy.resource_mapper"] = {
  get_authz_uris = function(_, _, _, project_id, _)
    captured_project_id_from_mapper = project_id
    return "resource", "action"
  end
}

package.loaded["kong.plugins.o11y-proxy.aws_signer"] = {
  sign_request = function() return { Authorization = "AWS4..." } end
}

-- Load handler
local handler = dofile(plugin_dir .. "handler.lua")

describe("handler.lua - access", function()

  before_each(function()
    -- Reset mocks
    mock_kong.ctx.shared = {}
    captured_project_id_from_mapper = nil
    mock_kong.request.get_header = function(name) return nil end
    mock_kong.service.set_target = spy.new(function() end)
    mock_kong.service.request.set_header = spy.new(function() end)

    mock_router.query_target = function() return {
        dns_address = "https://upstream.com",
        access_principal = "",
      }, nil end

    mock_authz.check_permission = function() return { has_permission = true }, nil end

    mock_authn.authenticate = spy.new(function()
      return { check_result = true, user_id = 12345 }, nil
    end)
  end)

  it("fails if account_user_meta header is missing and no authn service configured", function()
    mock_kong.request.get_header = function(name) return nil end

    local conf = { authz_service_url = "http://authz" }
    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_false(ok)
    assert.same({ status = 401, body = { message = "Authentication failed" } }, err)
  end)

  it("fails if account_user_meta header is invalid json", function()
    mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return "invalid_json" end
      return nil
    end

    local conf = { authz_service_url = "http://authz" }
    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_false(ok)
    assert.same({ status = 401, body = { message = "Authentication failed" } }, err)
  end)

  it("passes authentication if account_user_meta is valid", function()
    mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    local conf = {
      authz_service_url = "http://authz",
      router_service_url = "http://router"
    }

    -- Should not throw error
    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_true(ok, err)

    -- Verify router called
    assert.spy(mock_kong.service.set_target).was_called(1)
  end)

  it("falls back to authn service if account_user_meta is missing", function()
    mock_kong.request.get_header = function(name)
      if name == "Authorization" then return "Bearer test-token" end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    local conf = {
      authn_service_url = "http://authn",
      authz_service_url = "", -- don't test authz here
      router_service_url = "http://router",
    }

    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_true(ok, err)
    assert.spy(mock_authn.authenticate).was_called(1)
    assert.spy(mock_authn.authenticate).was_called_with("http://authn", "test-token", conf.authn_timeout, conf.authn_cache_ttl)
  end)

  it("returns 401 if authn service denies", function()
    mock_kong.request.get_header = function(name)
      if name == "Authorization" then return "test-token" end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    mock_authn.authenticate = spy.new(function()
      return { check_result = false, user_id = nil }, nil
    end)

    local conf = {
      authn_service_url = "http://authn",
      authz_service_url = "http://authz",
    }

    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_false(ok)
    assert.same({ status = 401, body = { message = "Authentication failed" } }, err)
  end)

  it("checks authorization if authz_service_url is configured", function()
     mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    local authz_called = false
    mock_authz.check_permission = function()
      authz_called = true
      return { has_permission = true }, nil
    end

    local conf = {
      authz_service_url = "http://authz",
      router_service_url = "http://router"
    }

    handler:access(conf)
    assert.is_true(authz_called)
  end)

  it("uses X-Current-Project-Id as project_id for authz when present", function()
     mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      if name == "X-Org-Id" then return "org-a" end
      if name == "X-Project-Id" then return "proj-legacy" end
      if name == "X-Current-Project-Id" then return "proj-current" end
      if name == "X-Application-Id" then return "cluster-a" end
      return nil
    end

    local conf = {
      authz_service_url = "http://authz",
      router_service_url = "http://router"
    }

    handler:access(conf)
    assert.equals("proj-current", captured_project_id_from_mapper)
  end)

  it("falls back to X-Project-Id when X-Current-Project-Id is missing", function()
     mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      if name == "X-Org-Id" then return "org-a" end
      if name == "X-Project-Id" then return "proj-legacy" end
      if name == "X-Current-Project-Id" then return nil end
      if name == "X-Application-Id" then return "cluster-a" end
      return nil
    end

    local conf = {
      authz_service_url = "http://authz",
      router_service_url = "http://router"
    }

    handler:access(conf)
    assert.equals("proj-legacy", captured_project_id_from_mapper)
  end)

  it("skips authorization if authz_service_url is not configured", function()
     mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    local authz_called = false
    mock_authz.check_permission = function()
      authz_called = true
      return { has_permission = true }, nil
    end

    local conf = {
      authz_service_url = "", -- Empty
      router_service_url = "http://router"
    }

    handler:access(conf)
    assert.is_false(authz_called)
  end)

  it("denies access if authorization fails", function()
      mock_kong.request.get_header = function(name)
      if name == "account_user_meta" then return '{"id": 12345}' end
      if name == "X-Biz-Type" then return "tidbcloud" end
      return nil
    end

    mock_authz.check_permission = function()
      return { has_permission = false }, nil
    end

    local conf = {
      authz_service_url = "http://authz",
      router_service_url = "http://router"
    }

    local ok, err = pcall(function() handler:access(conf) end)
    assert.is_false(ok)
    assert.same({ status = 403, body = { message = "Access denied: insufficient permissions" } }, err)
  end)

end)
