local constants = require "kong.constants"
local helpers = require "spec.helpers"
local websocket_client = require "resty.websocket.client"
local cjson = require "cjson"


local MESSAGE = "echo, ping, pong. echo, ping, pong. echo, ping, pong.\n"


local function websocket_send_text_and_get_echo(uri)
  local payload = { message = "hello websocket" }
  local wc = assert(websocket_client:new())
  assert(wc:connect(uri))

  assert(wc:send_text(cjson.encode(payload)))
  local frame, typ, err = wc:recv_frame()
  assert.is_nil(wc.fatal)
  assert(frame, err)
  assert.equal("text", typ)
  assert.same(payload, cjson.decode(frame))

  assert(wc:send_close())
end


for _, strategy in helpers.each_strategy() do

  -- Might need to be marked as flaky because it may require an arbitrary high port?
  describe("anonymous reports in Admin API #" .. strategy, function()
    local dns_hostsfile
    local reports_server

    local reports_send_ping = function(opts)
      ngx.sleep(0.01) -- hand over the CPU so other threads can do work (processing the sent data)
      local admin_client = helpers.admin_client()
      local opts = opts or {}
      local port = opts.port or nil

      local res = admin_client:post("/reports/send-ping" .. (port and "?port=" .. port or ""))
      assert.response(res).has_status(200)
      admin_client:close()
    end

    local reports_send_stream_ping = function()
      ngx.sleep(0.01) -- hand over the CPU so other threads can do work (processing the sent data)

      local tcp = ngx.socket.tcp()
      assert(tcp:connect(helpers.get_proxy_ip(false), 19001))

      local body = assert(tcp:receive("*a"))
      assert.equal("ok", body)

      tcp:close()
    end

    lazy_setup(function()
      dns_hostsfile = assert(os.tmpname() .. ".hosts")
      local fd = assert(io.open(dns_hostsfile, "wb"))
      assert(fd:write("127.0.0.1 " .. constants.REPORTS.ADDRESS))
      assert(fd:close())

      local bp = assert(helpers.get_db_utils(strategy, {
        "services",
        "routes",
        "plugins",
        "certificates",
        "snis",
      }, { "reports-api" }))

      local http_srv = assert(bp.services:insert {
        name = "mock-service",
        host = helpers.mock_upstream_host,
        port = helpers.mock_upstream_port,
      })

      bp.routes:insert({ service = http_srv,
                         protocols = { "http" },
                         hosts = { "http-service.test" } })

      bp.routes:insert({ service = http_srv,
                         protocols = { "https" },
                         hosts = { "https-service.test" } })

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

      bp.routes:insert({
        service = grpc_srv,
        protocols = { "grpc" },
        hosts = { "grpc" },
      })

      local grpcs_srv = bp.services:insert({
        name = "grpcs",
        url = helpers.grpcbin_ssl_url,
      })

      bp.routes:insert({
        service = grpcs_srv,
        protocols = { "grpcs" },
        hosts = { "grpcs" },
      })

      local ws_srv = bp.services:insert({
        name = "ws",
        path = "/ws",
      })

      bp.routes:insert({
        service = ws_srv,
        protocols = { "http" },
        paths = { "/up-ws" },
        strip_path = true,
      })

      local tcp_srv = bp.services:insert({
        name = "tcp",
        host = helpers.mock_upstream_host,
        port = helpers.mock_upstream_stream_port,
        protocol = "tcp"
      })

      bp.routes:insert {
        destinations = {
          { port = 19000, },
        },
        protocols = {
          "tcp",
        },
        service = tcp_srv,
      }

      local tls_srv = bp.services:insert({
        name = "tls",
        host = helpers.mock_upstream_host,
        port = helpers.mock_upstream_stream_ssl_port,
        protocol = "tls"
      })

      bp.routes:insert {
        destinations = {
          { port = 19443, },
        },
        protocols = {
          "tls",
        },
        service = tls_srv,
      }

      local reports_srv = bp.services:insert({
        name = "reports-srv",
        host = helpers.mock_upstream_host,
        port = helpers.mock_upstream_stream_port,
        protocol = "tcp"
      })

      bp.routes:insert {
        destinations = {
          { port = 19001, },
        },
        protocols = {
          "tcp",
        },
        service = reports_srv,
      }

      bp.plugins:insert({
        name = "reports-api",
        service = { id = reports_srv.id },
        protocols = { "tcp" },
        config = {}
      })

      assert(helpers.start_kong({
        nginx_conf = "spec/fixtures/custom_nginx.template",
        database = strategy,
        dns_hostsfile = dns_hostsfile,
        resolver_hosts_file = dns_hostsfile,
        anonymous_reports = true,
        plugins = "reports-api",
        stream_listen = helpers.get_proxy_ip(false) .. ":19000," ..
                        helpers.get_proxy_ip(false) .. ":19001," ..
                        helpers.get_proxy_ip(true)  .. ":19443 ssl",

      }))
    end)

    lazy_teardown(function()
      helpers.stop_kong()
      os.remove(dns_hostsfile)
    end)


    before_each(function()
      reports_server = helpers.tcp_server(constants.REPORTS.STATS_TLS_PORT, {tls=true})
    end)

    it("reports http requests", function()
      local proxy_client = assert(helpers.proxy_client())
      local res = proxy_client:get("/", {
        headers = { host  = "http-service.test" }
      })
      assert.response(res).has_status(200)

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=1", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)

      proxy_client:close()
    end)

    it("reports https requests", function()
      local proxy_ssl_client = assert(helpers.proxy_ssl_client())
      local res = proxy_ssl_client:get("/", {
        headers = { host  = "https-service.test" }
      })
      assert.response(res).has_status(200)

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=1", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)

      proxy_ssl_client:close()
    end)

    it("when send http request to https port, no other error in error.log", function()
      local https_port = assert(helpers.get_proxy_port(true))
      local proxy_client = assert(helpers.proxy_client(nil, https_port))
      local res = proxy_client:get("/", {
        headers = { host  = "http-service.test" }
      })
      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      assert.response(res).has_status(400)
      assert.logfile().has.no.line("using uninitialized")
      assert.logfile().has.no.line("could not determine log suffix (scheme=http, proxy_mode=)")
      proxy_client:close()
    end)

    it("reports h2c requests", function()
      local h2c_client = assert(helpers.proxy_client_h2c())
      local body, headers = h2c_client({
        headers = { [":authority"] = "http-service.test" }
      })

      assert.equal(200, tonumber(headers:get(":status")))
      assert.is_not_nil(body)

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=1", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)
    end)

    it("reports h2 requests", function()
      local h2_client = assert(helpers.proxy_client_h2())
      local body, headers = h2_client({
        headers = { [":authority"] = "https-service.test" }
      })

      assert.equal(200, tonumber(headers:get(":status")))
      assert.is_not_nil(body)

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=1", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)
    end)


    it("reports grpc requests", function()
      local grpc_client = helpers.proxy_client_grpc()
      assert(grpc_client({
        service = "hello.HelloService.SayHello",
        opts = {
          ["-authority"] = "grpc",
        },
      }))

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=1", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)
    end)

    it("reports grpcs requests", function()
      local grpcs_client = assert(helpers.proxy_client_grpcs())
      grpcs_client({
        service = "hello.HelloService.SayHello",
        opts = {
          ["-authority"] = "grpcs",
        },
      })

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=1", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=0", reports_data)
    end)

    it("reports ws requests", function()
      websocket_send_text_and_get_echo("ws://" .. helpers.get_proxy_ip(false) ..
                                       ":" .. helpers.get_proxy_port(false) .. "/up-ws")

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=1", reports_data)
      assert.match("wss_reqs=0", reports_data)
    end)

    it("reports wss requests", function()
      websocket_send_text_and_get_echo("wss://" .. helpers.get_proxy_ip(true) ..
                                       ":" .. helpers.get_proxy_port(true) .. "/up-ws")

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())
      assert.match("requests=1", reports_data)
      assert.match("http_reqs=0", reports_data)
      assert.match("https_reqs=0", reports_data)
      assert.match("h2c_reqs=0", reports_data)
      assert.match("h2_reqs=0", reports_data)
      assert.match("grpc_reqs=0", reports_data)
      assert.match("grpcs_reqs=0", reports_data)
      assert.match("ws_reqs=0", reports_data)
      assert.match("wss_reqs=1", reports_data)
    end)

    pending("#stream reports tcp streams", function()
      local tcp = ngx.socket.tcp()
      assert(tcp:connect(helpers.get_proxy_ip(false), 19000))

      assert(tcp:send(MESSAGE))

      local body = assert(tcp:receive("*a"))
      assert.equal(MESSAGE, body)

      tcp:close()

      reports_send_stream_ping()

      local _, reports_data = assert(reports_server:join())
      assert.match("streams=1", reports_data)
      assert.match("tcp_streams=1", reports_data)
      assert.match("tls_streams=0", reports_data)
    end)

    pending("#stream reports tls streams", function()
      local tcp = ngx.socket.tcp()

      assert(tcp:connect(helpers.get_proxy_ip(true), 19443))

      assert(tcp:sslhandshake(nil, "this-is-needed.test", false))

      assert(tcp:send(MESSAGE))

      local body = assert(tcp:receive("*a"))
      assert.equal(MESSAGE, body)

      tcp:close()

      reports_send_stream_ping()

      local _, reports_data = assert(reports_server:join())
      assert.match("streams=2", reports_data)
      assert.match("tcp_streams=1", reports_data) -- it counts the stream request for the ping
      assert.match("tls_streams=1", reports_data)
    end)

    it("does not log NGINX-produced errors", function()
      local proxy_client = assert(helpers.proxy_client())
      local res = assert(proxy_client:send {
        method = "GET",
        path = "/",
        headers = {
          ["X-Large"] = string.rep("a", 2^10 * 10), -- default large_client_header_buffers is 8k
        }
      })

      -- send a ping so the tcp server shutdown cleanly and not with a timeout.
      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      assert.res_status(400, res)
      proxy_client:close()

      assert.errlog()
            .has.no.line([[could not determine log suffix]], true)
    end)

    it("reports route statistics", function()
      local proxy_client = assert(helpers.proxy_client())
      local res = proxy_client:get("/", {
        headers = { host  = "http-service.test" }
      })
      assert.response(res).has_status(200)

      reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

      local _, reports_data = assert(reports_server:join())

      assert.match([["headers":0]], reports_data)
      assert.match([["routes":5]], reports_data)
      assert.match([["http":3]], reports_data)
      assert.match([["grpc":2]], reports_data)
      assert.match([["stream":0]], reports_data)
      assert.match([["tls_passthrough":0]], reports_data)
      assert.match([["flavor":"traditional_compatible"]], reports_data)
      assert.match([["paths":1]], reports_data)
      assert.match([["regex_routes":0]], reports_data)
      assert.match([["v1":0]], reports_data)
      assert.match([["v0":5]], reports_data)
      proxy_client:close()
    end)

    if strategy ~= "off" then
      it("reports route statistics after #change", function()
        local admin = helpers.admin_client()
        -- any other route will fail because we are ... routing all traffic to localhost
        assert.res_status(201, admin:send({
          method = "POST",
          path = "/services",
          body = {
            name = "test",
            url = "http://localhost:9001/services/",
            path = "/",
          },
          headers = { ["Content-Type"] = "application/json" },
        }))
        assert.res_status(201, admin:send({
          method = "POST",
          path = "/services/test/routes",
          body = {
            protocols = { "http" },
            headers = { ["x-test"] = { "test" } },
            paths = { "~/test", "/normal" },
            preserve_host = false,
            path_handling = "v1",
          },
          headers = { ["Content-Type"] = "application/json" },
        }))

        local proxy_client = assert(helpers.proxy_client())
        helpers.pwait_until(function()
          local res = proxy_client:get("/test", {
            headers = { ["x-test"] = "test", host = "http-service2.test" }
          })
          assert.response(res).has_status(200)
        end, 1000)

        for _ = 1, 2 do
          local res = proxy_client:get("/test", {
            headers = { ["x-test"] = "test", host = "http-service2.test" }
          })
          assert.response(res).has_status(200)
        end

        for _ = 1, 5 do
          local res = proxy_client:get("/foo", {
            headers = { ["x-test"] = "test", host = "http-service2.test" }
          })
          assert.response(res).has_status(404)
        end

        reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT})

        local _, reports_data = assert(reports_server:join())

        assert.match([["headers":1]], reports_data)
        assert.match([["routes":6]], reports_data)
        assert.match([["http":4]], reports_data)
        assert.match([["grpc":2]], reports_data)
        assert.match([["stream":0]], reports_data)
        assert.match([["tls_passthrough":0]], reports_data)
        assert.match([["flavor":"traditional_compatible"]], reports_data)
        assert.match([["paths":3]], reports_data)
        assert.match([["regex_routes":1]], reports_data)
        assert.match([["v1":1]], reports_data)
        assert.match([["v0":5]], reports_data)
        assert.match([[request_route_cache_hit_pos=2]], reports_data)
        -- the wait_util may trigger cache misses, so we can't assert on accurate values
        assert.match([[request_route_cache_hit_neg=]], reports_data)
        proxy_client:close()
      end)
    end
  end)
end
