/* global btoa fetch token promise_test step_timeout */
/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */

const templates = {
  'fresh': {
    'response_headers': [
      ['Expires', 100000],
      ['Last-Modified', 0]
    ]
  },
  'stale': {
    'response_headers': [
      ['Expires', -5000],
      ['Last-Modified', -100000]
    ]
  },
  'lcl_response': {
    'response_headers': [
      ['Location', 'location_target'],
      ['Content-Location', 'content_location_target']
    ]
  },
  'location': {
    'query_arg': 'location_target',
    'response_headers': [
      ['Expires', 100000],
      ['Last-Modified', 0]
    ]
  },
  'content_location': {
    'query_arg': 'content_location_target',
    'response_headers': [
      ['Expires', 100000],
      ['Last-Modified', 0]
    ]
  }
}

const noBodyStatus = new Set([204, 304])

function makeTest (test) {
  return function () {
    var uuid = token()
    var requests = expandTemplates(test)
    var fetchFunctions = makeFetchFunctions(requests, uuid)
    return runTest(fetchFunctions, test, requests, uuid)
  }
}

function makeFetchFunctions(requests, uuid) {
    var fetchFunctions = []
    for (let i = 0; i < requests.length; ++i) {
      var config = requests[i];
      if (config.skip) {
        // Skip request are ones that we expect the browser to make in
        // response to a redirect. We don't fetch them again, but
        // the server needs them in the config to be able to respond to
        // them.
        continue;
      }
      fetchFunctions.push({
        code: function (idx) {
          var config = requests[idx]
          var url = makeTestUrl(uuid, config);
          var init = fetchInit(requests, config)
          return fetch(url, init)
            .then(makeCheckResponse(idx, config))
            .then(makeCheckResponseBody(config, uuid), function (reason) {
              if ('expected_type' in config && config.expected_type === 'error') {
                assert_throws_js(TypeError, function () { throw reason })
              } else {
                throw reason
              }
            })
        },
        pauseAfter: 'pause_after' in requests[i]
      })
    }
    return fetchFunctions
}

function runTest(fetchFunctions, test, requests, uuid) {
    var idx = 0
    function runNextStep () {
      if (fetchFunctions.length) {
        var nextFetchFunction = fetchFunctions.shift()
        if (nextFetchFunction.pauseAfter === true) {
          return nextFetchFunction.code(idx++)
            .then(pause)
            .then(runNextStep)
        } else {
          return nextFetchFunction.code(idx++)
            .then(runNextStep)
        }
      } else {
        return Promise.resolve()
      }
    }

    return runNextStep()
      .then(function () {
        return getServerState(uuid)
      }).then(function (testState) {
        checkRequests(test, requests, testState)
        return Promise.resolve()
      })
}

function expandTemplates (test) {
  var rawRequests = test.requests
  var requests = []
  for (let i = 0; i < rawRequests.length; i++) {
    var request = rawRequests[i]
    request.name = test.name
    if ('template' in request) {
      var template = templates[request['template']]
      for (let member in template) {
        if (!request.hasOwnProperty(member)) {
          request[member] = template[member]
        }
      }
    }
    requests.push(request)
  }
  return requests
}

function fetchInit (requests, config) {
  var init = {
    'headers': []
  }
  if ('request_method' in config) init.method = config['request_method']
  // Note: init.headers must be a copy of config['request_headers'] array,
  // because new elements are added later.
  if ('request_headers' in config) init.headers = [...config['request_headers']];
  if ('name' in config) init.headers.push(['Test-Name', config.name])
  if ('request_body' in config) init.body = config['request_body']
  if ('mode' in config) init.mode = config['mode']
  if ('credentials' in config) init.credentials = config['credentials']
  if ('cache' in config) init.cache = config['cache']
  init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))])
  return init
}

function makeCheckResponse (idx, config) {
  return function checkResponse (response) {
    var reqNum = idx + 1
    var resNum = parseInt(response.headers.get('Server-Request-Count'))
    if ('expected_type' in config) {
      if (config.expected_type === 'error') {
        assert_true(false, `Request ${reqNum} doesn't throw an error`)
        return response.text()
      }
      if (config.expected_type === 'cached') {
        assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`)
      }
      if (config.expected_type === 'not_cached') {
        assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`)
      }
    }
    if ('expected_status' in config) {
      assert_equals(response.status, config.expected_status,
        `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`)
    } else if ('response_status' in config && config.response_status[0] != 301) {
      assert_equals(response.status, config.response_status[0],
        `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`)
    } else {
      assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`)
    }
    if ('response_headers' in config) {
      config.response_headers.forEach(function (header) {
        if (header.len < 3 || header[2] === true) {
          assert_equals(response.headers.get(header[0]), header[1],
            `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
        }
      })
    }
    if ('expected_response_headers' in config) {
      config.expected_response_headers.forEach(function (header) {
        assert_equals(response.headers.get(header[0]), header[1],
          `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
      })
    }
    return response.text()
  }
}

function makeCheckResponseBody (config, uuid) {
  return function checkResponseBody (resBody) {
    var statusCode = 200
    if ('response_status' in config) {
      statusCode = config.response_status[0]
    }
    if ('expected_response_text' in config) {
      if (config.expected_response_text !== null) {
        assert_equals(resBody, config.expected_response_text,
          `Response body is "${resBody}", not expected "${config.expected_response_text}"`)
      }
    } else if ('response_body' in config && config.response_body !== null) {
      assert_equals(resBody, config.response_body,
        `Response body is "${resBody}", not sent "${config.response_body}"`)
    } else if (!noBodyStatus.has(statusCode)) {
      assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`)
    }
  }
}

function checkRequests (test, requests, testState) {
  var testIdx = 0
  for (let i = 0; i < requests.length; ++i) {
    var expectedValidatingHeaders = []
    var config = requests[i]
    var serverRequest = testState[testIdx]
    var reqNum = i + 1
    if ('expected_type' in config) {
      if (config.expected_type === 'cached') continue // the server will not see the request
      if (config.expected_type === 'etag_validated') {
        expectedValidatingHeaders.push('if-none-match')
      }
      if (config.expected_type === 'lm_validated') {
        expectedValidatingHeaders.push('if-modified-since')
      }
    }
    testIdx++
    expectedValidatingHeaders.forEach(vhdr => {
      assert_own_property(serverRequest.request_headers, vhdr,
        `request ${reqNum} doesn't have ${vhdr} header`)
    })
    if ('expected_request_headers' in config) {
      config.expected_request_headers.forEach(expectedHdr => {
        assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1],
          `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`)
      })
    }
  }
  if (test?.check_count && testState) {
    assert_equals(requests.length, testState.length);
  }
}

function pause () {
  return new Promise(function (resolve, reject) {
    step_timeout(function () {
      return resolve()
    }, 3000)
  })
}

function makeTestUrl (uuid, config) {
  var arg = ''
  var base_url = ''
  if ('base_url' in config) {
    base_url = config.base_url
  }
  if ('query_arg' in config) {
    arg = `&target=${config.query_arg}`
  }
  if ('url_params' in config) {
    arg = `${arg}&${config.url_params}`
  }
  return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}`
}

function getServerState (uuid) {
  return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`)
    .then(function (response) {
      return response.text()
    }).then(function (text) {
      return JSON.parse(text) || []
    })
}

function run_tests (tests) {
  tests.forEach(function (test) {
    promise_test(makeTest(test), test.name)
  })
}

var contentStore = {}
function http_content (csKey) {
  if (csKey in contentStore) {
    return contentStore[csKey]
  } else {
    var content = btoa(Math.random() * Date.now())
    contentStore[csKey] = content
    return content
  }
}
