// test_parser.mbt — Tests for the RFC 8288 `Link` header parser (link_parser.mbt).

///|
let dflt : Limits = Limits::default()

///|
test "parse a single basic link" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"next\"", dflt),
  )
  assert_true(links.length() == 1)
  assert_str_eq(links[0].target(), "https://example.com/")
  assert_true(links[0].has_relation("next"))
  assert_true(links[0].anchor() is None)
  assert_true(links[0].extensions().is_empty())
}

///|
test "parse multiple links separated by comma" {
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"next\", ; rel=\"prev\", ; rel=\"canonical\"", dflt,
    ),
  )
  assert_true(links.length() == 3)
  assert_str_eq(links[0].target(), "a")
  assert_str_eq(links[1].target(), "b")
  assert_str_eq(links[2].target(), "c")
  assert_true(links[0].has_relation("next"))
  assert_true(links[1].has_relation("prev"))
  assert_true(links[2].has_relation("canonical"))
}

///|
test "parse every serialization-defined parameter, names case-insensitive" {
  // Parameter names use the mixed case. RFC 8288's grammar inherits ABNF
  // string literals (RFC 5234), which match case-insensitively: REL, ANCHOR,
  // HREFLANG, MEDIA, TITLE and TYPE are all recognized, and the canonical
  // serialization lowercases them.
  let links = unwrap_ok(
    parse_link_header(
      "; REL=\"alternate\"; ANCHOR=\"https://example.com/\"; HREFLANG=\"en\"; MEDIA=\"print\"; TITLE=\"The Doc\"; TYPE=\"text/html\"",
      dflt,
    ),
  )
  assert_true(links.length() == 1)
  let link = links[0]
  assert_str_eq(link.target(), "https://example.com/doc")
  assert_true(link.has_relation("alternate"))
  match link.anchor() {
    Some(a) => assert_str_eq(a, "https://example.com/")
    None => fail("anchor missing")
  }
  assert_true(link.hreflang().length() == 1)
  assert_str_eq(link.hreflang()[0], "en")
  match link.media() {
    Some(m) => assert_str_eq(m, "print")
    None => fail("media missing")
  }
  match link.title() {
    Some(t) => assert_str_eq(t, "The Doc")
    None => fail("title missing")
  }
  match link.media_type() {
    Some(t) => assert_str_eq(t, "text/html")
    None => fail("type missing")
  }
  assert_str_eq(
    serialize_link_header(links),
    "; rel=\"alternate\"; anchor=\"https://example.com/\"; hreflang=\"en\"; media=\"print\"; title=\"The Doc\"; type=\"text/html\"",
  )
}

///|
test "parse title* as an RFC 8187 extended value" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"next\"; title*=UTF-8'en'%E2%82%AC", dflt),
  )
  assert_true(links.length() == 1)
  match links[0].title_star() {
    Some(ev) => {
      assert_str_eq(ev.charset(), "UTF-8")
      match ev.language() {
        Some(l) => assert_str_eq(l, "en")
        None => fail("language missing")
      }
      assert_str_eq(ev.value(), "€")
    }
    None => fail("title* missing")
  }
}

///|
test "rel value with multiple relation types" {
  let links = unwrap_ok(parse_link_header("; rel=\"next prev last\"", dflt))
  assert_true(links.length() == 1)
  let names = links[0].relation_names()
  assert_true(names.length() == 3)
  assert_str_eq(names[0], "next")
  assert_str_eq(names[1], "prev")
  assert_str_eq(names[2], "last")
}

///|
test "rel is case-preserved but matched case-insensitively" {
  let links = unwrap_ok(parse_link_header("; rel=\"NEXT\"", dflt))
  assert_str_eq(links[0].relation_names()[0], "NEXT")
  assert_true(links[0].has_relation("next"))
  assert_true(links[0].has_relation("NEXT"))
}

///|
test "unquoted rel token form is accepted" {
  let links = unwrap_ok(parse_link_header("; rel=next", dflt))
  assert_true(links[0].has_relation("next"))
}

///|
test "extension relation type is an absolute URI" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"https://example.com/rel\"", dflt),
  )
  assert_true(links.length() == 1)
  assert_true(links[0].relations()[0].is_extension())
  assert_true(links[0].has_relation("https://example.com/rel"))
}

///|
test "hreflang is repeatable and preserved in order" {
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"alternate\"; hreflang=\"en\"; hreflang=\"de\"", dflt,
    ),
  )
  assert_true(links[0].hreflang().length() == 2)
  assert_str_eq(links[0].hreflang()[0], "en")
  assert_str_eq(links[0].hreflang()[1], "de")
}

///|
test "extension parameters are preserved with name and value" {
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"next\"; x-foo=bar; x-baz=\"quoted value\"", dflt,
    ),
  )
  assert_true(links[0].extensions().length() == 2)
  let ext = links[0].extensions()
  assert_str_eq(ext[0].name(), "x-foo")
  match ext[0].value() {
    Some(v) => assert_str_eq(v, "bar")
    None => fail("x-foo value missing")
  }
  assert_false(ext[0].quoted())
  assert_str_eq(ext[1].name(), "x-baz")
  match ext[1].value() {
    Some(v) => assert_str_eq(v, "quoted value")
    None => fail("x-baz value missing")
  }
  assert_true(ext[1].quoted())
}

///|
test "flag extension parameter (no value) is preserved" {
  let links = unwrap_ok(parse_link_header("; rel=\"next\"; x-flag", dflt))
  assert_true(links[0].extensions().length() == 1)
  assert_str_eq(links[0].extensions()[0].name(), "x-flag")
  assert_true(links[0].extensions()[0].value() is None)
}

///|
test "deprecated rev is preserved as an extension parameter" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"next\"; rev=\"previous\"", dflt),
  )
  assert_true(links[0].extensions().length() == 1)
  assert_str_eq(links[0].extensions()[0].name(), "rev")
  match links[0].extensions()[0].value() {
    Some(v) => assert_str_eq(v, "previous")
    None => fail("rev value missing")
  }
}

///|
test "single-occurrence parameters are first-wins and recorded as duplicates" {
  let parsed = unwrap_ok(
    parse_link_header_detailed(
      "; rel=\"next\"; title=\"first\"; title=\"second\"", dflt,
    ),
  )
  match parsed.links()[0].title() {
    Some(t) => assert_str_eq(t, "first")
    None => fail("title missing")
  }
  assert_true(parsed.duplicates().length() == 1)
  assert_str_eq(parsed.duplicates()[0], "title")
}

///|
test "duplicate rel is first-wins and recorded" {
  let parsed = unwrap_ok(
    parse_link_header_detailed("; rel=\"next\"; rel=\"prev\"", dflt),
  )
  assert_true(parsed.links()[0].has_relation("next"))
  assert_false(parsed.links()[0].has_relation("prev"))
  assert_str_eq(parsed.duplicates()[0], "rel")
}

///|
test "duplicate names are recorded only once" {
  let parsed = unwrap_ok(
    parse_link_header_detailed(
      "; rel=\"next\"; title=\"1\"; title=\"2\"; title=\"3\"", dflt,
    ),
  )
  assert_true(parsed.duplicates().length() == 1)
}

///|
test "empty list elements are allowed and ignored" {
  let links = unwrap_ok(parse_link_header(", ; rel=\"next\", ,", dflt))
  assert_true(links.length() == 1)
  assert_str_eq(links[0].target(), "a")
}

///|
test "leading and trailing whitespace is tolerated" {
  let links = unwrap_ok(parse_link_header("  ; rel=\"next\"  ", dflt))
  assert_true(links.length() == 1)
}

///|
test "whitespace around the semicolon and equals is tolerated" {
  let links = unwrap_ok(
    parse_link_header(" ; rel = \"next\" ; title = \"T\"", dflt),
  )
  assert_true(links.length() == 1)
  assert_true(links[0].has_relation("next"))
}

///|
test "empty input raises EmptyInput" {
  expect_err_kind(parse_link_header("", dflt), EmptyInput)
  expect_err_kind(parse_link_header("   ", dflt), EmptyInput)
  expect_err_kind(parse_link_header(", ,", dflt), EmptyInput)
}

///|
test "missing rel parameter raises an error" {
  assert_true(err_stage(parse_link_header("", dflt)) == LinkValue)
  assert_true(err_kind(parse_link_header("", dflt)) == InvalidRelation)
}

///|
test "unterminated target raises UnterminatedTarget" {
  expect_err_kind(
    parse_link_header("; rel=\"next\"", dflt),
    ExpectedAngleBracket,
  )
}

///|
test "space inside the target is rejected" {
  // The space is the 13th byte of the input (0-based offset 12).
  assert_true(
    err_offset(parse_link_header("; rel=\"next\"", dflt)) ==
    12,
  )
}

///|
test "trailing garbage after a link raises TrailingInput" {
  expect_err_kind(
    parse_link_header("; rel=\"next\" xyz", dflt),
    TrailingInput,
  )
}

///|
test "empty rel value raises InvalidRelation" {
  expect_err_kind(parse_link_header("; rel=\"\"", dflt), InvalidRelation)
}

///|
test "rel containing invalid token characters is rejected" {
  expect_err_kind(
    parse_link_header("; rel=\"bad[rel\"", dflt),
    InvalidRelation,
  )
}

///|
test "hreflang with invalid language tag is rejected" {
  expect_err_kind(
    parse_link_header("; rel=\"next\"; hreflang=\"e\"", dflt),
    InvalidLanguageTag,
  )
}

///|
test "anchor with invalid URI-reference is rejected" {
  expect_err_kind(
    parse_link_header("; rel=\"next\"; anchor=\"has space\"", dflt),
    InvalidContextValue,
  )
}

///|
test "invalid media type in type parameter is rejected" {
  expect_err_kind(
    parse_link_header("; rel=\"next\"; type=\"not a type\"", dflt),
    InvalidMediaType,
  )
}

///|
test "title* with non-extended value is rejected" {
  // A `name*` parameter is always parsed as an RFC 8187 ext-value, so a
  // quoted-string value there is a malformed ext-value.
  expect_err_kind(
    parse_link_header("; rel=\"next\"; title*=\"plain\"", dflt),
    UnexpectedCharacter,
  )
  assert_true(
    err_stage(parse_link_header("; rel=\"next\"; title*=\"plain\"", dflt)) ==
    ExtendedValue,
  )
}

///|
test "target may be an IRI (non-ASCII bytes)" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"next\"", dflt),
  )
  assert_str_eq(links[0].target(), "https://example.com/αβγ")
}

///|
test "error context is bounded to max_context_bytes" {
  let long = "<" + rng_long_target()
  assert_true(
    err_context(parse_link_header(long, dflt)).length() <= max_context_bytes(),
  )
}

///|
test "error display is a single line with stage, kind, offset" {
  expect_err_kind(parse_link_header("", dflt), EmptyInput)
  let d = err_display(parse_link_header("", dflt))
  assert_true(d.contains("Input"))
  assert_true(d.contains("EmptyInput"))
  assert_true(d.contains("byte 0"))
}

///|
test "quoted-string contents can contain commas and semicolons" {
  let links = unwrap_ok(
    parse_link_header("; rel=\"next\"; title=\"a, b; c\"", dflt),
  )
  match links[0].title() {
    Some(t) => assert_str_eq(t, "a, b; c")
    None => fail("title missing")
  }
}

///|
test "quoted-pair escapes resolve inside quoted strings" {
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"next\"; title=\"say \\\"hi\\\" and \\\\\"", dflt,
    ),
  )
  match links[0].title() {
    Some(t) => assert_str_eq(t, "say \"hi\" and \\")
    None => fail("title missing")
  }
}

///|
test "type parameter may carry media-type parameters" {
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"next\"; type=\"text/html; charset=utf-8\"", dflt,
    ),
  )
  match links[0].media_type() {
    Some(t) => assert_str_eq(t, "text/html; charset=utf-8")
    None => fail("type missing")
  }
}

///|
test "parse result of RFC 8288 example header" {
  // The example from RFC 8288 Section 3.1.
  let links = unwrap_ok(
    parse_link_header(
      "; rel=\"http://example.net/foo\"; anchor=\"http://example.net/\"", dflt,
    ),
  )
  assert_true(links.length() == 1)
  assert_str_eq(links[0].target(), "/")
  assert_true(links[0].has_relation("http://example.net/foo"))
  match links[0].anchor() {
    Some(a) => assert_str_eq(a, "http://example.net/")
    None => fail("anchor missing")
  }
}

///|
/// A target string long enough to trigger context truncation.
fn rng_long_target() -> String {
  let sb = StringBuilder()
  for i = 0; i < 500; i = i + 1 {
    sb.write_char('x')
  }
  sb.write_char('>')
  sb.to_string()
}