// test_serializer.mbt — Tests for the deterministic serializer (link_serializer.mbt)
// and canonicalization (canonicalize.mbt).

///|
test "serialize_link emits target then rel first" {
  let link = web_link("/page")
  link.relations.push(Registered("next"))
  assert_str_eq(serialize_link(link), "; rel=\"next\"")
}

///|
test "serialize_link_header joins links with comma-space" {
  let links : Array[WebLink] = []
  let a = web_link("/a")
  a.relations.push(Registered("next"))
  let b = web_link("/b")
  b.relations.push(Registered("prev"))
  links.push(a)
  links.push(b)
  assert_str_eq(
    serialize_link_header(links),
    "; rel=\"next\", ; rel=\"prev\"",
  )
}

///|
test "serialize_link_header of an empty array is empty" {
  assert_str_eq(serialize_link_header([]), "")
}

///|
test "serialize every target attribute in deterministic order" {
  let link : WebLink = {
    target: "https://example.com/doc",
    relations: [Registered("alternate")],
    anchor: Some("https://example.com/"),
    hreflang: ["en", "de"],
    media: Some("print"),
    title: Some("The Doc"),
    title_star: None,
    media_type: Some("text/html"),
    extensions: [{ name: "x-tag", value: Some("v1"), quoted: false }],
  }
  assert_str_eq(
    serialize_link(link),
    "; rel=\"alternate\"; anchor=\"https://example.com/\"; hreflang=\"en\"; hreflang=\"de\"; media=\"print\"; title=\"The Doc\"; type=\"text/html\"; x-tag=v1",
  )
}

///|
test "extension parameter value uses token form when possible" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: None,
    media_type: None,
    extensions: [{ name: "x-note", value: Some("plain"), quoted: false }],
  }
  assert_str_eq(serialize_link(link), "; rel=\"next\"; x-note=plain")
}

///|
test "extension parameter value is quoted when it cannot be a token" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: None,
    media_type: None,
    extensions: [{ name: "x-note", value: Some("hello world"), quoted: false }],
  }
  assert_str_eq(
    serialize_link(link),
    "; rel=\"next\"; x-note=\"hello world\"",
  )
}

///|
test "flag extension parameter serializes without a value" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: None,
    media_type: None,
    extensions: [{ name: "x-flag", value: None, quoted: false }],
  }
  assert_str_eq(serialize_link(link), "; rel=\"next\"; x-flag")
}

///|
test "title* serializes as an RFC 8187 extended value" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: Some({ charset: "UTF-8", language: Some("en"), value: "hello" }),
    media_type: None,
    extensions: [],
  }
  assert_str_eq(
    serialize_link(link),
    "; rel=\"next\"; title*=UTF-8'en'hello",
  )
}

///|
test "title* with percent-encoding of non-ASCII value" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: Some({ charset: "UTF-8", language: None, value: "café" }),
    media_type: None,
    extensions: [],
  }
  assert_str_eq(
    serialize_link(link),
    "; rel=\"next\"; title*=UTF-8''caf%C3%A9",
  )
}

///|
test "name* extension parameter serializes as an unquoted ext-value" {
  let link : WebLink = {
    target: "a",
    relations: [Registered("next")],
    anchor: None,
    hreflang: [],
    media: None,
    title: None,
    title_star: None,
    media_type: None,
    extensions: [
      { name: "x-enc*", value: Some("UTF-8'en'caf%C3%A9"), quoted: false },
    ],
  }
  assert_str_eq(
    serialize_link(link),
    "; rel=\"next\"; x-enc*=UTF-8'en'caf%C3%A9",
  )
}

///|
test "serialize_quoted_string escapes quotes and backslashes" {
  assert_str_eq(serialize_quoted_string("plain"), "\"plain\"")
  assert_str_eq(serialize_quoted_string("a \"b\" \\c"), "\"a \\\"b\\\" \\\\c\"")
  assert_str_eq(serialize_quoted_string(""), "\"\"")
}

///|
test "can_be_token decides between token and quoted forms" {
  assert_true(can_be_token("abc"))
  assert_true(can_be_token("a-b_c.d"))
  assert_false(can_be_token("a/b")) // "/" is a separator, not a tchar
  assert_false(can_be_token("has space"))
  assert_false(can_be_token(""))
}

///|
test "canonicalize_link_header normalizes whitespace and quoting" {
  let canonical = unwrap_ok(
    canonicalize_link_header(";rel=\"next\", ; rel = \"prev\""),
  )
  assert_str_eq(canonical, "; rel=\"next\", ; rel=\"prev\"")
}

///|
test "canonicalize_link_header drops empty list elements" {
  let canonical = unwrap_ok(canonicalize_link_header(", ; rel=\"next\", ,"))
  assert_str_eq(canonical, "; rel=\"next\"")
}

///|
test "canonicalize_link_header is idempotent" {
  let x = ";rel=\"next\";title=\"T\" ,  ; rel=prev"
  let once = unwrap_ok(canonicalize_link_header(x))
  let twice = unwrap_ok(canonicalize_link_header(once))
  assert_str_eq(once, twice)
}

///|
test "canonicalize_link_value canonicalizes a single link-value" {
  let canonical = unwrap_ok(canonicalize_link_value(";rel=\"next\"  "))
  assert_str_eq(canonical, "; rel=\"next\"")
}

///|
test "canonicalize_link_header returns parse errors unchanged" {
  expect_err_kind(canonicalize_link_header("not a link"), ExpectedAngleBracket)
}

///|
test "serialize then parse round-trips a full-featured link" {
  let link : WebLink = {
    target: "https://example.com/doc",
    relations: [Registered("alternate"), Extension("https://e.test/rel")],
    anchor: Some("https://example.com/"),
    hreflang: ["en", "de"],
    media: Some("print"),
    title: Some("The Doc"),
    title_star: Some({
      charset: "UTF-8",
      language: Some("en"),
      value: "héllo wörld",
    }),
    media_type: Some("text/html"),
    extensions: [
      { name: "x-tag", value: Some("v1"), quoted: false },
      { name: "x-note", value: Some("a value"), quoted: true },
    ],
  }
  let header = serialize_link(link)
  let links = unwrap_ok(parse_link_header(header, Limits::default()))
  assert_true(links.length() == 1)
  assert_links_eq(links, [link])
}