// Internal unit tests for the middleware core (T-03). Runs as part of the
// package's internal test suite:
//   moon test --target native -p vicoproplus/moon_ua_parser_middleware_core
//
// Golden cases are quoted verbatim from the vendored uap-core test
// snapshots (`uap-core/tests/`, read-only); synthetic failure injections
// are marked `synthetic: 失败注入专用`.

///|
/// Pin the empty-UaInfo definition (design decision, task-3 brief): all
/// three families are the EMPTY string (meaning "no parse result"),
/// every version/brand/model field None — distinct from the library's
/// `"Other"` all-miss fallback (evidence on `FALLBACK_FAMILY`).
test "empty_ua_info definition: empty families, None versions" {
  let info = empty_ua_info()
  assert_eq("b=\{info.browser.family} o=\{info.os.family} d=\{info.device.family}", "b= o= d=")
  assert_eq(info.browser.major, None)
  assert_eq(info.browser.patch_minor, None)
  assert_eq(info.os.major, None)
  assert_eq(info.device.brand, None)
  assert_eq(info.device.model, None)
  // The empty definition must NOT itself be judged empty-fallback:
  // "" != "Other" keeps the two "nothing" states distinguishable.
  assert_eq(is_empty_fallback(info), false)
}

///|
/// Golden healthy case: all three domains match real families, so
/// `assemble` mounts the parsed UaInfo unchanged with no forensics.
///
/// golden:uap-core-tests uap-core/tests/test_ua.yaml:8955 (case id
/// 'DuckDuckGo/7 Safari/605.1.15' -> family 'DuckDuckGo Mobile', major 7);
/// golden:uap-core-tests uap-core/tests/test_os.yaml:3302 (same UA ->
/// family 'iOS', major 14, minor 3); golden:uap-core-tests
/// uap-core/tests/test_device.yaml:80537 (same UA -> family 'iPhone',
/// brand 'Apple', model 'iPhone').
test "assemble keeps a fully matched parse (DuckDuckGo iPhone golden)" {
  let ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.3 Mobile/15E148 DuckDuckGo/7 Safari/605.1.15"
  match @ua_parser.parse(ua) {
    Ok(info) => {
      // Three-group results: non-empty AND non-fallback families.
      assert_eq("browser=\{info.browser.family}", "browser=DuckDuckGo Mobile")
      assert_eq(info.browser.family == FALLBACK_FAMILY, false)
      assert_eq(info.browser.major, Some("7"))
      assert_eq("os=\{info.os.family}", "os=iOS")
      assert_eq(info.os.family == FALLBACK_FAMILY, false)
      assert_eq(info.os.major, Some("14"))
      assert_eq(info.os.minor, Some("3"))
      assert_eq("device=\{info.device.family}", "device=iPhone")
      assert_eq(info.device.family == FALLBACK_FAMILY, false)
      assert_eq(info.device.brand, Some("Apple"))
      // Healthy assembly: parsed info passes through untouched.
      let outcome = assemble(ua, Ok(info))
      assert_eq(outcome.degraded, false)
      assert_eq(outcome.ua_info == info, true)
      match outcome.forensics {
        Some(_) => fail("healthy parse must not produce forensics")
        None => ()
      }
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
}

///|
/// Second golden healthy case (Android handset), also asserted through the
/// `is_degraded` judgment on the parse result.
///
/// golden:uap-core-tests uap-core/tests/test_ua.yaml:170 (case id
/// 'Amaze_4G Build/IML74K' -> browser family 'Android', 4.0.3);
/// golden:uap-core-tests uap-core/tests/test_os.yaml:122 (same UA ->
/// family 'Android', 4.0.3); golden:uap-core-tests
/// uap-core/tests/test_device.yaml:43 (same UA -> family
/// 'HTC Amaze 4G', brand 'HTC', model 'Amaze 4G').
test "assemble keeps a fully matched parse (HTC Amaze 4G golden)" {
  let ua = "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; Amaze_4G Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"
  let result = @ua_parser.parse(ua)
  assert_eq(is_degraded(result), false)
  match result {
    Ok(info) => {
      assert_eq("browser=\{info.browser.family}", "browser=Android")
      assert_eq("os=\{info.os.family}", "os=Android")
      assert_eq("device=\{info.device.family}", "device=HTC Amaze 4G")
      let outcome = assemble(ua, result)
      assert_eq(outcome.degraded, false)
      assert_eq(outcome.ua_info == info, true)
      assert_eq(outcome.forensics is None, true)
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
}

///|
/// Golden partial-fallback case: only the device domain falls back
/// (desktop browsers have no device rule), browser and OS match — so the
/// result stays HEALTHY (degradation requires all three domains empty).
///
/// golden:uap-core-tests uap-core/tests/test_ua.yaml:731 (case id
/// 'NCSA_Mosaic/2.0 (Windows 3.1)' -> family 'NCSA Mosaic', major 2);
/// golden:uap-core-tests uap-core/tests/test_os.yaml:850 (same UA ->
/// family 'Windows', major 3.1); golden:uap-core-tests
/// uap-core/tests/test_device.yaml:398 (same UA -> device fallback
/// family 'Other', no brand/model).
test "partial fallback (device only) stays healthy (NCSA Mosaic golden)" {
  let ua = "NCSA_Mosaic/2.0 (Windows 3.1)"
  match @ua_parser.parse(ua) {
    Ok(info) => {
      assert_eq("browser=\{info.browser.family}", "browser=NCSA Mosaic")
      assert_eq("os=\{info.os.family}", "os=Windows")
      // The device domain IS at the fallback literal here.
      assert_eq(info.device.family, FALLBACK_FAMILY)
      assert_eq(is_empty_fallback(info), false)
      let outcome = assemble(ua, Ok(info))
      assert_eq(outcome.degraded, false)
      assert_eq(outcome.ua_info == info, true)
      assert_eq(outcome.forensics is None, true)
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
}

///|
/// Golden ALL-fallback case — the empirical R3 verification: this UA
/// matches no rule in any domain, and the library reports the fallback
/// literal "Other" for ALL THREE families (see `FALLBACK_CONSTANT`
/// evidence). `assemble` must degrade: mount empty UaInfo + forensics.
///
/// golden:uap-core-tests uap-core/tests/test_ua.yaml:1020 (case id
/// 'SomethingWeNeverKnewExisted' -> browser family 'Other', no versions);
/// golden:uap-core-tests uap-core/tests/test_os.yaml:710 (same UA -> OS
/// family 'Other'); the device domain has no snapshot entry for this UA —
/// the library's no-rule fallback applies (engine.mbt:63-65, uap-core has
/// no device catch-all), and this test verifies that empirically at
/// runtime below.
test "all-fallback parse degrades to empty UaInfo (SomethingWeNeverKnewExisted golden)" {
  let ua = "SomethingWeNeverKnewExisted"
  let result = @ua_parser.parse(ua)
  match result {
    Ok(info) => {
      // Empirical fallback-value verification (all three domains).
      assert_eq(info.browser.family, "Other")
      assert_eq(info.os.family, "Other")
      assert_eq(info.device.family, "Other")
      assert_eq(is_empty_fallback(info), true)
      let outcome = assemble(ua, result)
      assert_eq(outcome.degraded, true)
      assert_eq(outcome.ua_info == empty_ua_info(), true)
      match outcome.forensics {
        Some(rec) => {
          assert_eq(rec.ua_summary, ua) // 27 chars, below the 64 cap
          assert_eq(rec.reason, REASON_EMPTY_FALLBACK)
          assert_eq(rec.reason.length() > 0, true)
        }
        None => fail("degraded outcome must carry a forensics record")
      }
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
}

///|
/// synthetic: 失败注入专用 — the Err branch of the degradation contract.
/// `@ua_parser.UaError` is a read-only error type (external code cannot
/// construct `Err` values), so the Err arm is pinned through
/// `assemble_error`, which `assemble` calls on its `Err(_)` arm: mounts
/// empty UaInfo, records `parse_error` forensics, does NOT raise.
test "synthetic Err result degrades with parse_error forensics" {
  let ua = "Mozilla/4.0 (compatible; synthetic-failure-injection)"
  let outcome = assemble_error(ua)
  assert_eq(outcome.degraded, true)
  assert_eq(outcome.ua_info == empty_ua_info(), true)
  match outcome.forensics {
    Some(rec) => {
      assert_eq(rec.ua_summary, ua)
      assert_eq(rec.reason, REASON_PARSE_ERROR)
      assert_eq(rec.reason.length() > 0, true)
    }
    None => fail("Err outcome must carry a forensics record")
  }
  // An Ok-but-healthy result is NOT degraded (judgment contrast).
  assert_eq(is_degraded(Ok(empty_ua_info())), false)
}

///|
/// synthetic: 失败注入专用 — an 80-character malformed UA (only '#' bytes,
/// guaranteed to match no uap-core rule) flowing through the full
/// pipeline: parse falls back in all three domains, so `assemble`
/// degrades and the forensics ua_summary is truncated to exactly the
/// first 64 characters of the original UA.
test "synthetic malformed UA degrades with truncated 64-char summary" {
  let buf = StringBuilder()
  for i = 0; i < 80; i = i + 1 {
    buf.write_char('#')
  }
  let ua = buf.to_string()
  assert_eq(ua.length(), 80)
  let result = @ua_parser.parse(ua)
  match result {
    Ok(info) => {
      assert_eq(is_empty_fallback(info), true)
      let outcome = assemble(ua, result)
      assert_eq(outcome.degraded, true)
      match outcome.forensics {
        Some(rec) => {
          assert_eq(rec.ua_summary.length(), 64)
          assert_eq(rec.ua_summary, truncate_ua_summary(ua))
          assert_eq(rec.reason, REASON_EMPTY_FALLBACK)
        }
        None => fail("degraded outcome must carry a forensics record")
      }
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
}

///|
/// Boundary (R5): a UA of exactly 64 characters passes through
/// `truncate_ua_summary` and `make_forensics` unchanged.
test "boundary: 64-char UA summary kept verbatim" {
  let buf = StringBuilder()
  for i = 0; i < 64; i = i + 1 {
    buf.write_char('a')
  }
  let ua = buf.to_string()
  assert_eq(ua.length(), 64)
  assert_eq(truncate_ua_summary(ua), ua)
  let rec = make_forensics(ua, REASON_EMPTY_FALLBACK)
  assert_eq(rec.ua_summary, ua)
  assert_eq(rec.ua_summary.length(), UA_SUMMARY_MAX_LENGTH)
}

///|
/// Boundary (R5): a UA of exactly 65 characters is truncated to exactly
/// the first 64 characters.
test "boundary: 65-char UA summary truncated to 64" {
  let buf = StringBuilder()
  for i = 0; i < 65; i = i + 1 {
    buf.write_char('b')
  }
  let ua = buf.to_string()
  assert_eq(ua.length(), 65)
  let truncated = truncate_ua_summary(ua)
  assert_eq(truncated.length(), 64)
  let buf64 = StringBuilder()
  for i = 0; i < 64; i = i + 1 {
    buf64.write_char('b')
  }
  assert_eq(truncated, buf64.to_string())
  let rec = make_forensics(ua, REASON_EMPTY_FALLBACK)
  assert_eq(rec.ua_summary.length(), UA_SUMMARY_MAX_LENGTH)
}

///|
/// Boundary (R5): the empty-string UA — classified as degraded and
/// asserted through the full degradation path (all-miss fallback ->
/// empty UaInfo + forensics with an empty summary).
test "boundary: empty-string UA degrades with empty summary" {
  let result = @ua_parser.parse("")
  match result {
    Ok(info) => {
      // Empirical: no rule matches the empty string in any domain.
      assert_eq(info.browser.family, "Other")
      assert_eq(info.os.family, "Other")
      assert_eq(info.device.family, "Other")
    }
    Err(_) => fail("parse never fails with the precompiled rule set")
  }
  assert_eq(is_degraded(result), true)
  let outcome = assemble("", result)
  assert_eq(outcome.degraded, true)
  assert_eq(outcome.ua_info == empty_ua_info(), true)
  match outcome.forensics {
    Some(rec) => {
      assert_eq(rec.ua_summary, "")
      assert_eq(rec.reason, REASON_EMPTY_FALLBACK)
      assert_eq(rec.reason.length() > 0, true)
    }
    None => fail("degraded outcome must carry a forensics record")
  }
}