local cjson = require "cjson"
local helpers = require "spec.helpers"

-- returns nth byte (0: LSB, 3: MSB if 32-bit)
local function nbyt(x, n)
  return bit.band(bit.rshift(x, 8*n), 0xff)
end

local function be_bytes(x)
  return nbyt(x, 3), nbyt(x, 2), nbyt(x, 1), nbyt(x, 0)
end

for _, strategy in helpers.each_strategy() do

  describe("gRPC-Web Proxying [#" .. strategy .. "]", function()
    local proxy_client
    local proxy_client_ssl

    local HELLO_REQUEST_TEXT_BODY = "AAAAAAYKBGhleWE="
    local HELLO_REQUEST_BODY = ngx.decode_base64(HELLO_REQUEST_TEXT_BODY)
    local HELLO_RESPONSE_TEXT_BODY = "AAAAAAwKCmhlbGxvIGhleWE=" ..
        "gAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo="
    local HELLO_RESPONSE_BODY = ngx.decode_base64("AAAAAAwKCmhlbGxvIGhleWE=") ..
        ngx.decode_base64("gAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=")

    lazy_setup(function()
      local bp = helpers.get_db_utils(strategy, {
        "routes",
        "services",
        "plugins",
      }, {
        "grpc-web",
      })

      local service1 = assert(bp.services:insert {
        name = "grpc",
        url = helpers.grpcbin_url,
      })

      local route1 = assert(bp.routes:insert {
        protocols = { "http", "https" },
        paths = { "/" },
        service = service1,
      })

      local route2 = assert(bp.routes:insert {
        protocols = { "http", "https" },
        paths = { "/prefix" },
        service = service1,
      })

      local mock_grpc_service = assert(bp.services:insert {
        name = "mock_grpc_service",
        url = "http://localhost:8765",
      })

      local mock_grpc_route = assert(bp.routes:insert {
        protocols = { "http" },
        hosts = { "grpc_mock.example" },
        service = mock_grpc_service,
        preserve_host = true,
      })

      assert(bp.plugins:insert {
        route = mock_grpc_route,
        name = "grpc-web",
        config = {
        },
      })

      assert(bp.plugins:insert {
        route = route1,
        name = "grpc-web",
        config = {
          proto = "spec/fixtures/grpc/hello.proto",
        },
      })

      assert(bp.plugins:insert {
        route = route2,
        name = "grpc-web",
        config = {
          proto = "spec/fixtures/grpc/hello.proto",
          pass_stripped_path = true,
        },
      })

      local fixtures = {
        http_mock = {}
      }
      fixtures.http_mock.my_server_block = [[
        server {
          server_name myserver;
          listen 8765;
          location ~ / {
            content_by_lua_block {
              local headers = ngx.req.get_headers()
              ngx.header.content_type = "application/grpc"
              ngx.header.received_host = headers["Host"]
              ngx.header.received_te = headers["te"]
            }
          }
        }
      ]]

      assert(helpers.start_kong({
        database = strategy,
        plugins = "bundled,grpc-web",
        nginx_conf = "spec/fixtures/custom_nginx.template",
      }, nil, nil, fixtures))
    end)

    before_each(function()
      proxy_client = helpers.proxy_client(1000)
      proxy_client_ssl = helpers.proxy_ssl_client(1000)
    end)

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


    test("Sets 'TE: trailers'", function()
      local res, err = proxy_client:post("/", {
        headers = {
          ["Host"] = "grpc_mock.example",
          ["Content-Type"] = "application/grpc-web-text",
        },
      })

      assert.equal("trailers", res.headers["received-te"])
      assert.is_nil(err)
    end)

    test("Ignores user-agent TE", function()
      -- in grpc-web, kong acts as a grpc client on behalf of the client
      -- (which generally is a web-browser); as such, the Te header must be
      -- set by kong, which will append trailers to the response body
      local res, err = proxy_client:post("/", {
        headers = {
          ["Host"] = "grpc_mock.example",
          ["Content-Type"] = "application/grpc-web-text",
          ["TE"] = "chunked",
        },
      })

      assert.equal("trailers", res.headers["received-te"])
      assert.is_nil(err)
    end)

    test("Call gRCP-base64 via HTTP", function()
      local res, err = proxy_client:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/grpc-web-text",
          ["Content-Length"] = tostring(#HELLO_REQUEST_TEXT_BODY),
        },
        body = HELLO_REQUEST_TEXT_BODY,
      })

      assert.equal(HELLO_RESPONSE_TEXT_BODY, res:read_body())
      assert.is_nil(err)
    end)

    test("Call gRCP-base64 via HTTPS", function()
      local res, err = proxy_client_ssl:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/grpc-web-text",
          ["Content-Length"] = tostring(#HELLO_REQUEST_TEXT_BODY),
        },
        body = HELLO_REQUEST_TEXT_BODY,
      })

      assert.equal(HELLO_RESPONSE_TEXT_BODY, res:read_body())
      assert.is_nil(err)
    end)

    test("Call binary gRCP via HTTP", function()
      local res, err = proxy_client:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/grpc-web+proto",
          ["Content-Length"] = tostring(#HELLO_REQUEST_BODY),
        },
        body = HELLO_REQUEST_BODY,
      })

      assert.equal(HELLO_RESPONSE_BODY, res:read_body())
      assert.is_nil(err)
    end)

    test("Call binary gRCP via HTTPS", function()
      local res, err = proxy_client_ssl:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/grpc-web+proto",
          ["Content-Length"] = tostring(#HELLO_REQUEST_BODY),
        },
        body = HELLO_REQUEST_BODY,
      })

      assert.equal(HELLO_RESPONSE_BODY, res:read_body())
      assert.is_nil(err)
    end)

    test("Call gRPC-Web JSON via HTTP", function()
      local req = cjson.encode{ greeting = "heya" }
      req = string.char(0, be_bytes(#req)) .. req
      local res, err = proxy_client:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/grpc-web+json",
          ["Content-Length"] = tostring(#req)
        },
        body = req,
      })

      local resp = cjson.encode{ reply = "hello heya" }
      resp = string.char(0, be_bytes(#resp)) .. resp

      local trailer = "grpc-status:0\r\ngrpc-message:\r\n"
      trailer = string.char(0x80, be_bytes(#trailer)) .. trailer

      assert.equal(resp .. trailer, res:read_body())
      assert.is_nil(err)
    end)

     test("Call plain JSON via HTTP", function()
      local res, err = proxy_client:post("/hello.HelloService/SayHello", {
        headers = {
          ["Content-Type"] = "application/json",
        },
        body = cjson.encode{ greeting = "heya" },
      })

      assert.same({ reply = "hello heya" }, cjson.decode((res:read_body())))
      assert.is_nil(err)
    end)

     test("Pass stripped URI", function()
       local res, err = proxy_client:post("/prefix/hello.HelloService/SayHello", {
         headers = {
           ["Content-Type"] = "application/json",
         },
         body = cjson.encode{ greeting = "heya" },
       })

       assert.same({ reply = "hello heya" }, cjson.decode((res:read_body())))
       assert.is_nil(err)
     end)
 end)
end
