local helpers = require("spec.helpers")
local cjson = require("cjson")
local pl_file = require("pl.file")

local PLUGIN_NAME = "ai-proxy"

for _, strategy in helpers.all_strategies() do
  if strategy ~= "cassandra" then
    describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function()
      local client
      local MOCK_PORT

      lazy_setup(function()
        MOCK_PORT = helpers.get_available_port()

        local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME })

        -- set up huggingface mock fixtures
        local fixtures = {
          http_mock = {},
        }

        fixtures.http_mock.huggingface = [[
        server {
          server_name huggingface;
          listen ]] .. MOCK_PORT .. [[;
          
          default_type 'application/json';

          location = "/v1/chat/completions" {
            content_by_lua_block {
              local pl_file = require "pl.file"
              local json = require("cjson.safe")

              local token = ngx.req.get_headers()["authorization"]
              if token == "Bearer huggingface-key" then
                ngx.req.read_body()
                local body, err = ngx.req.get_body_data()
                body, err = json.decode(body)
                
                if err or (body.messages == ngx.null) then
                  ngx.status = 400
                  ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/responses/bad_request.json"))
                else
                  ngx.status = 200
                  ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/responses/good.json"))
                end
              else
                ngx.status = 401
                ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/responses/unauthorized.json"))
              end
            }
          }

          # completions is on the root of huggingface models
          location = "/" {
            content_by_lua_block {
              local pl_file = require "pl.file"
              local json = require("cjson.safe")

              local token = ngx.req.get_headers()["authorization"]
              if token == "Bearer huggingface-key" then
                ngx.req.read_body()
                local body, err = ngx.req.get_body_data()
                body, err = json.decode(body)
                
                if err or (body.prompt == ngx.null) then
                  ngx.status = 400
                  ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-completions/responses/bad_request.json"))
                else
                  ngx.status = 200
                  ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-completions/responses/good.json"))
                end
              else
                ngx.status = 401
                ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-completions/responses/unauthorized.json"))
              end
            }
          }
          location = "/model-loading/v1/chat/completions" {
            content_by_lua_block {
              local pl_file = require "pl.file"
                
              ngx.status = 503
              ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/responses/bad_response_model_load.json"))
            }
          }
          location = "/model-timeout/v1/chat/completions" {
            content_by_lua_block {
              local pl_file = require "pl.file"
                
              ngx.status = 504
              ngx.print(pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/responses/bad_response_timeout.json"))
            }
          }
        }
      ]]

        local empty_service = assert(bp.services:insert({
          name = "empty_service",
          host = "localhost", --helpers.mock_upstream_host,
          port = 8080, --MOCK_PORT,
          path = "/",
        }))

        -- 200 chat good with one option
        local chat_good = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/chat/good" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = chat_good.id },
          config = {
            route_type = "llm/v1/chat",
            auth = {
              header_name = "Authorization",
              header_value = "Bearer huggingface-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = true,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT,
              },
            },
          },
        })
        local completions_good = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/completions/good" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = completions_good.id },
          config = {
            route_type = "llm/v1/completions",
            auth = {
              header_name = "Authorization",
              header_value = "Bearer huggingface-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = true,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT,
              },
            },
          },
        })
        -- 401 unauthorized
        local chat_401 = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/chat/unauthorized" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = chat_401.id },
          config = {
            route_type = "llm/v1/chat",
            auth = {
              header_name = "api-key",
              header_value = "wrong-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = true,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT,
              },
            },
          },
        })
        -- 401 unauthorized
        local completions_401 = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/completions/unauthorized" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = completions_401.id },
          config = {
            route_type = "llm/v1/completions",
            auth = {
              header_name = "api-key",
              header_value = "wrong-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = true,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT,
              },
            },
          },
        })
        -- 503 Service Temporarily Unavailable
        local chat_503 = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/chat/bad-response/model-loading" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = chat_503.id },
          config = {
            route_type = "llm/v1/chat",
            auth = {
              header_name = "api-key",
              header_value = "huggingface-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = false,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT.."/model-loading",
              },
            },
          },
        })
        -- 503 Service Timeout
        local chat_503_to = assert(bp.routes:insert({
          service = empty_service,
          protocols = { "http" },
          strip_path = true,
          paths = { "/huggingface/llm/v1/chat/bad-response/model-timeout" },
        }))
        bp.plugins:insert({
          name = PLUGIN_NAME,
          route = { id = chat_503_to.id },
          config = {
            route_type = "llm/v1/chat",
            auth = {
              header_name = "api-key",
              header_value = "huggingface-key",
            },
            model = {
              name = "mistralai/Mistral-7B-Instruct-v0.2",
              provider = "huggingface",
              options = {
                max_tokens = 256,
                temperature = 1.0,
                huggingface = {
                  use_cache = false,
                  wait_for_model = false,
                },
                upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORT.."/model-timeout",
              },
            },
          },
        })

        -- start kong
        assert(helpers.start_kong({
          -- set the strategy
          database = strategy,
          -- use the custom test template to create a local mock server
          nginx_conf = "spec/fixtures/custom_nginx.template",
          -- make sure our plugin gets loaded
          plugins = "bundled," .. PLUGIN_NAME,
          -- write & load declarative config, only if 'strategy=off'
          declarative_config = strategy == "off" and helpers.make_yaml_file() or nil,
        }, nil, nil, fixtures))
      end)

      lazy_teardown(function()
        helpers.stop_kong()
      end)

      before_each(function()
        client = helpers.proxy_client()
      end)

      after_each(function()
        if client then
          client:close()
        end
      end)

      describe("huggingface llm/v1/chat", function()
        it("good request", function()
          local r = client:get("/huggingface/llm/v1/chat/good", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/requests/good.json"),
          })
          -- validate that the request succeeded, response status 200
          local body = assert.res_status(200, r)
          local json = cjson.decode(body)

          -- check this is in the 'kong' response format
          assert.equals(json.model, "mistralai/Mistral-7B-Instruct-v0.2")
          assert.equals(json.object, "chat.completion")

          assert.is_table(json.choices)
          --print("json: ", inspect(json))
          assert.is_string(json.choices[1].message.content)
          assert.same(
            " The sum of 1 + 1 is 2. This is a basic arithmetic operation and the answer is always the same: adding one to one results in having two in total.",
            json.choices[1].message.content
          )
        end)
      end)
      describe("huggingface llm/v1/completions", function()
        it("good request", function()
          local r = client:get("/huggingface/llm/v1/completions/good", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-completions/requests/good.json"),
          })

          -- validate that the request succeeded, response status 200
          local body = assert.res_status(200, r)
          local json = cjson.decode(body)

          -- check this is in the 'kong' response format
          assert.equals("mistralai/Mistral-7B-Instruct-v0.2", json.model)
          assert.equals("llm/v1/completions", json.object)

          assert.is_table(json.choices)
          assert.is_table(json.choices[1])
          assert.same("I am a language model AI created by Mistral AI", json.choices[1].message.content)
        end)
      end)
      describe("huggingface no auth", function()
        it("unauthorized request chat", function()
          local r = client:get("/huggingface/llm/v1/chat/unauthorized", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/requests/good.json"),
          })

          local body = assert.res_status(401, r)
          local json = cjson.decode(body)
          assert.equals(json.error, "Authorization header is correct, but the token seems invalid")
        end)
        it("unauthorized request completions", function()
          local r = client:get("/huggingface/llm/v1/completions/unauthorized", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-completions/requests/good.json"),
          })

          local body = assert.res_status(401, r)
          local json = cjson.decode(body)
          assert.equals(json.error, "Authorization header is correct, but the token seems invalid")
        end)
      end)
      describe("huggingface bad request", function()
        it("bad chat request", function()
          local r = client:get("/huggingface/llm/v1/chat/good", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = { messages = ngx.null },
          })

          local body = assert.res_status(400, r)
          local json = cjson.decode(body)
          assert.equals(json.error.message, "request body doesn't contain valid prompts")
        end)
        it("bad completions request", function()
          local r = client:get("/huggingface/llm/v1/completions/good", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = { prompt = ngx.null },
          })

          local body = assert.res_status(400, r)
          local json = cjson.decode(body)
          assert.equals(json.error.message, "request body doesn't contain valid prompts")
        end)
      end)
      describe("huggingface bad response", function()
        it("bad chat response", function()
          local r = client:get("/huggingface/llm/v1/chat/bad-response/model-loading", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/requests/good.json"),
          })

          local body = assert.res_status(503, r)
          local json = cjson.decode(body)
          assert.equals(json.error, "Model mistralai/Mistral-7B-Instruct-v0.2 is currently loading")
        end)
        it("bad completions request", function()
          local r = client:get("/huggingface/llm/v1/chat/bad-response/model-timeout", {
            headers = {
              ["content-type"] = "application/json",
              ["accept"] = "application/json",
            },
            body = pl_file.read("spec/fixtures/ai-proxy/huggingface/llm-v1-chat/requests/good.json"),
          })
          local body = assert.res_status(504, r)
          local json = cjson.decode(body)
          assert.equals(json.error, "Model mistralai/Mistral-7B-Instruct-v0.2 time out")
        end)
      end)
    end)
  end
end
