local n = require('test.functional.testnvim')()
local t = require('test.testutil')
local describe, it, before_each = t.describe, t.it, t.before_each
local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1'

local exec_lua = n.exec_lua

---@param method vim.net.HttpMethod
---@param opts? vim.net.request.Opts
---@return table
--- Helper method to make a HTTP request with a 2s timeout
local function request(method, url, opts)
  opts = opts or {}
  opts.retry = 3
  local result = exec_lua(function()
    local done = false
    local result

    vim.net.request(method, url, opts, function(err, res)
      if err then
        result = { error = err }
      else
        ---@type string|table
        local resp

        local ok, parsed = pcall(vim.json.decode, res.body)
        if ok then
          resp = parsed
        else
          resp = res.body
        end
        result = { error = nil, response = resp }
      end
      done = true
    end)

    vim.wait(2000, function()
      return done
    end)
    return result
  end)

  return result
end

describe('vim.net.request', function()
  before_each(function()
    n:clear()
  end)

  it('fetches a URL into memory (async success)', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    ---@type table
    local result = request('GET', 'https://httpbingo.org/anything')

    t.eq(nil, result.error, ('request failed: %s'):format(result.error))
    t.eq('https://httpbingo.org/anything', result.response.url)
  end)

  it("detects filetype, sets 'nomodified'", function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local rv = exec_lua(function()
      vim.cmd('runtime! plugin/nvim/net.lua')
      vim.cmd('runtime! filetype.lua')
      -- github raw dump of a small lua file in the neovim repo
      vim.cmd(
        'edit https://raw.githubusercontent.com/neovim/neovim/master/runtime/syntax/tutor.lua'
      )
      vim.wait(2000, function()
        return vim.bo.filetype ~= ''
      end)
      -- wait for buffer to have content
      vim.wait(2000, function()
        return vim.fn.wordcount().bytes > 0
      end)
      vim.wait(2000, function()
        return vim.bo.modified == false
      end)
      return { vim.bo.filetype, vim.bo.modified }
    end)

    t.eq('lua', rv[1])
    t.eq(false, rv[2], 'Expected buffer to be unmodified for remote content')
  end)

  it('calls on_response with error on 404 (async failure)', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local result = request('GET', 'https://httpbingo.org/status/404')
    t.matches('404', result.error)
  end)

  it('plugin writes output to buffer', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local content = exec_lua(function()
      ---@type string[]
      local lines

      local buf = vim.api.nvim_create_buf(false, true)
      ---@diagnostic disable-next-line: param-type-mismatch
      vim.net.request('https://httpbingo.org', { outbuf = buf })

      vim.wait(2000, function()
        lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
        return lines[1] ~= ''
      end)

      return lines
    end)
    assert(content and content[1]:find('html'))
  end)

  it('works with :read', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local content = exec_lua(function()
      vim.cmd('runtime plugin/net.lua')
      ---@type string[]
      local lines

      vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'Here is some text' })
      vim.cmd(':read https://example.com')

      vim.wait(2000, function()
        lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
        return #lines > 1
      end)

      return lines
    end)

    t.eq(true, content ~= nil)
    t.eq(true, content[1]:find('Here') ~= nil)
    t.eq(true, content[2]:find('html') ~= nil)
  end)

  it('opens remote tar.gz URLs as tar archives', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local rv = exec_lua(function()
      vim.cmd('runtime! plugin/net.lua')
      vim.cmd('runtime! plugin/tarPlugin.vim')

      vim.cmd(
        'edit https://github.com/neovim/neovim/releases/download/nightly/nvim-macos-x86_64.tar.gz'
      )

      vim.wait(2500, function()
        return vim.bo.filetype == 'tar' or vim.b.tarfile ~= nil
      end)

      return {
        filetype = vim.bo.filetype,
        modified = vim.bo.modified,
        tarfile = vim.b.tarfile ~= nil,
      }
    end)

    t.eq('tar', rv.filetype)
    t.eq(false, rv.modified)
    t.eq(true, rv.tarfile)
  end)

  it('opens remote zip URLs as zip archives', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local rv = exec_lua(function()
      vim.cmd('runtime! plugin/net.lua')
      vim.cmd('runtime! plugin/zipPlugin.vim')

      vim.cmd('edit https://github.com/neovim/neovim/releases/download/nightly/nvim-win-arm64.zip')

      vim.wait(2500, function()
        return vim.bo.filetype == 'zip' or vim.b.zipfile ~= nil
      end)

      return {
        filetype = vim.bo.filetype,
        modified = vim.bo.modified,
        zipfile = vim.b.zipfile ~= nil,
      }
    end)

    t.eq('zip', rv.filetype)
    t.eq(false, rv.modified)
    t.eq(true, rv.zipfile)
  end)

  it('accepts custom headers', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')
    ---@type table
    local result = request('GET', 'https://httpbingo.org/anything', {
      headers = {
        Authorization = 'Bearer test-token',
        ['X-Custom-Header'] = 'custom-value',
        ['Empty'] = '',
      },
    })

    t.eq(nil, result.error, ('request failed: %s'):format(result.error))
    t.eq('table', type(result.response.headers), 'Expected headers to be a table')

    -- httpbingo.org/request returns each header as a list in the returned value
    t.eq(
      'Bearer test-token',
      result.response.headers.Authorization[1],
      'Expected Authorization header'
    )
    t.eq('custom-value', result.response.headers['X-Custom-Header'][1], 'Expected X-Custom-Header')
    t.eq('', result.response.headers['Empty'][1], 'Expected Empty header')
  end)

  it('accepts multiple HTTP methods', function()
    t.skip(skip_integ, 'NVIM_TEST_INTEG not set (network integration test)')

    local url = 'https://httpbingo.org/anything'

    local function assert_accept_method(method)
      local result = request(method, url)
      t.eq(nil, result.error)
      t.eq(method, result.response.method)
    end

    assert_accept_method('GET')
    assert_accept_method('PUT')
    assert_accept_method('PATCH')
    assert_accept_method('DELETE')

    -- HEAD request
    local result = request('HEAD', url)
    t.eq(nil, result.error)

    -- testing body payload
    result = request('POST', url, {
      body = '{"a": 1}',
      headers = {
        ['Content-Type'] = 'application/json',
      },
    })
    t.eq(nil, result.error)
    t.eq(1, result.response.json.a)
  end)

  it('validation', function()
    local function assert_wrong_request(expected_err, method, opts)
      if type(method) ~= 'string' then
        opts = method
        method = 'GET'
      end

      local result = t.pcall_err(exec_lua, function()
        vim.net.request(method, 'https://example.com', opts)
      end)
      t.matches(expected_err, result)
    end

    -- headers asserts
    assert_wrong_request('opts.headers: expected table, got number', { headers = 123 })

    --- FIXME(ellisonleao): this special assert is failing because the opts table is putting [""] in
    --- the key value instead of [123] upon calling the helper method
    -- assert_wrong_request(
    --   'headers keys and values must be strings',
    --   { headers = { [123] = 'value' } }
    -- )

    assert_wrong_request('headers keys and values must be strings', { headers = { Header = 123 } })
    assert_wrong_request(
      'header keys must not start with @ or end with : and ;',
      { headers = { ['Header:'] = 'value' } }
    )
    assert_wrong_request(
      'header keys must not start with @ or end with : and ;',
      { headers = { ['Header;'] = 'value' } }
    )
    assert_wrong_request(
      'header keys must not start with @ or end with : and ;',
      { headers = { ['@filename'] = '' } }
    )

    -- body asserts
    assert_wrong_request(
      'opts.body: expected body should be string and not start with @',
      { body = 123 }
    )
    assert_wrong_request(
      'opts.body: expected body should be string and not start with @',
      { body = {} }
    )
    assert_wrong_request(
      'opts.body: expected body should be string and not start with @',
      { body = '@test' }
    )

    -- OPTIONS is not accepted
    assert_wrong_request(
      'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got OPTIONS',
      'OPTIONS'
    )
    -- lowercase methods are not accepted as well
    assert_wrong_request(
      'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got options',
      'options'
    )
    assert_wrong_request(
      'expected method should be one of GET, POST, PUT, PATCH, HEAD, DELETE, got get',
      'get'
    )
  end)
end)
