"""Policy-level tests for the shared text-mark engine config.

These assert calibrated TUNING, not algorithm behaviour --
they exist so a future edit cannot silently revert a calibrated constant back to a
value that was measured to be wrong. The measurements themselves live in
`docs/module-internals.md` and in the comment at
`_text_mark_engine._DEFAULT_PROVENANCE_NCC_FACTOR`.
"""


class TestRivalMargin:
    """Detection among same-corner marks is COMPETITIVE, not just absolute.

    Doubao "豆包AI生成" and Jimeng "★ 即梦AI" both sit bottom-right in near-white CJK
    and survive binarization as similar blobs, so an absolute NCC gate cannot tell
    them apart because many Jimeng false additions were Doubao marks. Calibration
    showed that the relative template margin separates them without reducing genuine
    Jimeng detections.
    """

    def test_jimeng_competes_against_doubao(self):
        from remove_ai_watermarks import jimeng_engine

        assert "doubao_alpha.png" in jimeng_engine._CONFIG.rivals

    def test_doubao_has_no_rival_margin(self):
        """Asymmetric by measurement, not oversight: the symmetric gate cost Doubao 7
        genuine detections to prevent 5 false ones (1.4:1 against), while Jimeng gained
        25pp for free. Doubao's absolute detector is already 86% precise."""
        from remove_ai_watermarks import doubao_engine

        assert doubao_engine._CONFIG.rivals == ()

    def test_a_doubao_shaped_blob_loses_the_jimeng_margin(self):
        """The decisive case: a blob matching Doubao BETTER than Jimeng must not be
        booked as a Jimeng wordmark, however high its absolute Jimeng score."""
        import numpy as np

        from remove_ai_watermarks import jimeng_engine
        from remove_ai_watermarks._text_mark_engine import glyph_silhouette

        eng = jimeng_engine.JimengEngine()
        doubao_blob = glyph_silhouette("doubao_alpha.png")
        assert doubao_blob is not None
        canvas = np.zeros((doubao_blob.shape[0] + 20, doubao_blob.shape[1] + 20), np.uint8)
        canvas[10 : 10 + doubao_blob.shape[0], 10 : 10 + doubao_blob.shape[1]] = doubao_blob
        width = int(doubao_blob.shape[1] / jimeng_engine._CONFIG.alpha_width_frac)
        jimeng_score = eng._template_match_score(canvas, width)
        assert not eng._rival_margin_ok(jimeng_score, canvas, width)


class TestPerMarkProvenanceRelaxation:
    """The provenance NCC relaxation is PER MARK, not one shared multiplier.

    Calibration showed that one shared relaxation factor was too permissive for
    Jimeng because its relaxed silhouette confuses other bottom-right text marks.
    """


class TestScaleBasis:
    """Mark geometry scales with a PER-MARK image dimension, measured not assumed.

    Every tuned fraction was calibrated on PORTRAIT captures, where width and short
    side coincide, so the basis was never exercised until landscape inputs were
    measured. Calibration showed that a width-scaled Doubao box is inflated by the
    aspect ratio on a wide image and can miss the glyph. A short-side basis recovered
    the affected landscape cases. The same switch broke Jimeng, whose wordmark tracks
    the width, hence per-mark rather than a house rule.
    """

    def test_doubao_scales_with_the_short_side(self):
        from remove_ai_watermarks import doubao_engine

        assert doubao_engine._CONFIG.scale_basis == "short"

    def test_jimeng_scales_with_width(self):
        """Measured, not an oversight: the short-side basis took jimeng's labelled
        landscape positives from 13/13 to 0/13."""
        from remove_ai_watermarks import jimeng_engine

        assert jimeng_engine._CONFIG.scale_basis == "width"

    def test_samsung_keeps_width_because_it_is_unmeasured(self):
        """There is no calibration evidence for changing this basis."""
        from remove_ai_watermarks import samsung_engine

        assert samsung_engine._CONFIG.scale_basis == "width"

    def test_basis_only_differs_on_non_square_images(self):
        """The basis is a no-op wherever width IS the short side, which is why the bug
        survived: every calibration capture was portrait."""
        import numpy as np

        from remove_ai_watermarks import doubao_engine, jimeng_engine

        portrait = np.zeros((1600, 900, 3), np.uint8)
        landscape = np.zeros((900, 1600, 3), np.uint8)
        d, j = doubao_engine.DoubaoEngine(), jimeng_engine.JimengEngine()
        assert d.scale_base(portrait) == j.scale_base(portrait) == 900
        assert d.scale_base(landscape) == 900
        assert j.scale_base(landscape) == 1600

    def test_landscape_box_stays_inside_the_frame(self):
        """The concrete failure: a width-scaled box on a wide image overshoots the
        mark's real footprint. The short-side box must be proportionally smaller."""
        import numpy as np

        from remove_ai_watermarks import doubao_engine

        landscape = np.zeros((900, 2400, 3), np.uint8)
        loc = doubao_engine.DoubaoEngine().locate(landscape)
        assert loc.w < int(2400 * doubao_engine._CONFIG.width_frac)
        assert loc.x + loc.w <= 2400
        assert loc.y + loc.h <= 900


class TestTophatFrontend:
    """Detection can correlate the CONTINUOUS top-hat instead of a binarized blob.

    `extract_mask` thresholds the top-hat into a 0/255 glyph blob, which is fine for a
    mark stamped bold and opaque and destructive for a faint one -- a thin translucent
    overlay shatters into specks and no template can match a blob that is not there
    (measured: 千问 scored 0.170 mean vs doubao's 0.723 through the binary path, 0% over
    the gate). The `tophat` front-end never binarizes: the saturation/luma gates become
    weights, and the response is max-normalized so the score is contrast-invariant.

    Calibration showed improved Doubao recall without reducing precision.
    """

    def test_doubao_uses_the_continuous_frontend(self):
        from remove_ai_watermarks import doubao_engine

        assert doubao_engine._CONFIG.detect_frontend == "tophat"

    def test_other_marks_stay_binary_until_measured(self):
        """A front-end switch must be measured per mark before it ships; jimeng and
        samsung have no such measurement yet."""
        from remove_ai_watermarks import jimeng_engine, samsung_engine

        assert jimeng_engine._CONFIG.detect_frontend == "binary"
        assert samsung_engine._CONFIG.detect_frontend == "binary"

    def test_response_is_contrast_invariant(self):
        """The whole point: a faint mark and a bold one produce the same response, so a
        single threshold works for both. Binarizing is what loses the faint one."""
        import numpy as np

        from remove_ai_watermarks import doubao_engine

        eng = doubao_engine.DoubaoEngine()
        h, w = 400, 900
        out = []
        for amplitude in (12, 90):  # a barely-there overlay and a bold one
            img = np.full((h, w, 3), 100, np.uint8)
            loc = eng.locate(img)
            x, y, bw, bh = loc.bbox
            img[y + bh // 3 : y + 2 * bh // 3, x + bw // 4 : x + 3 * bw // 4] = 100 + amplitude
            resp = eng.tophat_response(img, loc)
            assert resp is not None
            out.append(resp)
        # max-normalized, so the two responses agree despite a 7.5x contrast difference
        assert abs(int(out[0].max()) - int(out[1].max())) <= 1

    def test_flat_input_yields_no_response(self):
        """A blank corner has no top-hat at all; the engine must return None rather
        than divide by a zero peak."""
        import numpy as np

        from remove_ai_watermarks import doubao_engine

        eng = doubao_engine.DoubaoEngine()
        img = np.full((400, 900, 3), 128, np.uint8)
        assert eng.tophat_response(img, eng.locate(img)) is None

    def test_threshold_is_frontend_specific(self):
        """The continuous front-end scores higher overall (0.809 vs 0.723 mean on the
        same positives), so it needs its own gate; the binary-era 0.40 left the
        provenance-relaxed gate low enough to admit 8 false fires where 0.50 admits 1."""
        from remove_ai_watermarks import doubao_engine

        assert doubao_engine._CONFIG.detect_ncc_threshold == 0.50
