// AR-002 — minimum face size, in original video resolution.
//
// TRACES: AR-002 | SR-002 | UT-002
//
// Tier T1 here, T2 in test_replay_fixtures.cpp. The requirement is arithmetic on
// bounding boxes, so the two edge cases that matter — a face sitting exactly on
// the threshold, and the same face seen through a downscaled decode — are
// reachable without a detector, a model or a GPU. What the fixture check adds is
// that the rule was actually applied on the way to a dump; what this adds is that
// it is applied *correctly*, which no real dump can demonstrate because real
// footage does not contain a 39.999 px face on demand.
//
// FaceDetectorFunc is never constructed: its constructor loads SCRFD. Only the
// static rule is called, so make_face_detector() is never odr-used and nothing
// here needs a backend.
#include <catch2/catch_test_macros.hpp>

#include "nodes/face_detector_node.hpp"
#include "types.hpp"

#include <vector>

namespace {

DetectedFace box(float w, float h) {
    DetectedFace f;
    f.bbox = cv::Rect2f(10.f, 10.f, w, h);
    f.confidence = 0.9f;
    return f;
}

// Sizes the rule kept, in the order given.
std::vector<float> surviving_widths(std::vector<DetectedFace> faces,
                                    float min_face_px, float bbox_upscale) {
    FaceDetectorFunc::drop_undersized(faces, min_face_px, bbox_upscale);
    std::vector<float> out;
    for (const auto& f : faces) out.push_back(f.bbox.width);
    return out;
}

constexpr float kMin = 40.f;   // Config::min_face_px default, and AR-002's number

}  // namespace

// ── Exactly at the threshold ─────────────────────────────────────────────────
// The boundary case is the whole content of a minimum: "40x40" has to mean 40 is
// admissible, or the requirement says 41.
TEST_CASE("a face exactly at the minimum is kept", "[detector][AR-002]") {
    CHECK(surviving_widths({box(kMin, kMin)}, kMin, 1.f).size() == 1);
}

TEST_CASE("a face one tenth of a pixel under the minimum is dropped",
          "[detector][AR-002]") {
    CHECK(surviving_widths({box(39.9f, 100.f)}, kMin, 1.f).empty());
    CHECK(surviving_widths({box(100.f, 39.9f)}, kMin, 1.f).empty());
}

TEST_CASE("both sides must clear the minimum, not the larger one",
          "[detector][AR-002]") {
    // A wide, short box has enough pixels and is still unusable: ArcFace
    // alignment needs both dimensions. Area would admit this; the rule must not.
    CHECK(surviving_widths({box(400.f, 20.f)}, kMin, 1.f).empty());
}

TEST_CASE("the filter is a filter, not a reordering", "[detector][AR-002]") {
    auto kept = surviving_widths(
        {box(80.f, 80.f), box(10.f, 10.f), box(60.f, 60.f), box(39.f, 39.f)},
        kMin, 1.f);
    REQUIRE(kept.size() == 2);
    // Order is load-bearing downstream (AR-003's largest-first sort tie-breaks on
    // it, and the Hungarian solver tie-breaks on index) — erase-remove must not
    // shuffle the survivors.
    CHECK(kept[0] == 80.f);
    CHECK(kept[1] == 60.f);
}

// ── The dense_scale interaction the requirement exists for ───────────────────
// dense_scale 0.5 halves the decoded frame, so the detector reports a 40 px face
// as 20 px. If the threshold were applied to those numbers, turning on a
// throughput knob would silently double the minimum face size the pipeline
// accepts — a recall change with no line in the config to explain it. AR-002
// pins the minimum to the ORIGINAL resolution instead.
TEST_CASE("at dense_scale 0.5 the cutoff stays 40 px of original footage",
          "[detector][AR-002]") {
    constexpr float kUpscale = 2.f;   // frame_source_node: 1 / dense_scale

    // 20 px in downscaled space is exactly 40 px of original footage: kept.
    CHECK(surviving_widths({box(20.f, 20.f)}, kMin, kUpscale).size() == 1);

    // 19.9 px downscaled is 39.8 px original: dropped.
    CHECK(surviving_widths({box(19.9f, 19.9f)}, kMin, kUpscale).empty());

    // And the interaction stated as one claim: a face of a given original size is
    // admitted or refused identically whether or not the decode was downscaled.
    for (float original : {30.f, 39.f, 40.f, 41.f, 80.f}) {
        INFO("original size " << original);
        const bool full  = !surviving_widths({box(original, original)},
                                             kMin, 1.f).empty();
        const bool dense = !surviving_widths({box(original / kUpscale,
                                                  original / kUpscale)},
                                             kMin, kUpscale).empty();
        CHECK(full == dense);
        CHECK(full == (original >= kMin));
    }
}

TEST_CASE("an absent or degenerate upscale falls back to the raw threshold",
          "[detector][AR-002]") {
    // bbox_upscale is 1 on every non-dense frame; 0 would mean the frame source
    // never set it. Dividing by that would reject every face in the film, which
    // is a failure worth not having.
    CHECK(surviving_widths({box(kMin, kMin)}, kMin, 0.f).size() == 1);
    CHECK(surviving_widths({box(39.f, 39.f)}, kMin, 0.f).empty());
}
