import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import build  # noqa: E402
import lens  # noqa: E402
import render  # noqa: E402

WORDS = ("alpha bravo charlie delta echoes foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec "
         "romeo sierra tango uniform victor whiskey xray yankee zulu").split()


def text(i, shared=0, base=None):
    """A 30-word answer unique to i; the first `shared` words copied from `base`'s answer."""
    own = ["w%d%s" % (i, WORDS[k % len(WORDS)]) for k in range(30)]
    own = [w.replace("0", "o").replace("1", "l").replace("2", "z").replace("3", "e").replace("4", "a").replace("5", "s")
           .replace("6", "b").replace("7", "t").replace("8", "q").replace("9", "g") for w in own]
    if base is not None and shared:
        own[:shared] = text(base).split()[:shared]
    return " ".join(own)


def A(frm, ep, ts, t):
    return {"from": frm, "epoch": ep, "ts": ts, "answer": t}


class TestIndependentBaseline(unittest.TestCase):
    def setUp(self):
        ans = {}
        for i in range(40):
            ans["t%02d" % i] = A("acct%d" % i, i % 4, 1000 + i, text(i, shared=6 if i % 5 == 0 and i else 0, base=0))
        ans["copy"] = A("ringA", 2, 5000, text(1))          # near-copy of t01 (acct1, E1): ring, excluded
        ans["same_author"] = A("acct2", 3, 6000, text(2))   # same author: never a pair
        ans["same_epoch"] = A("acct9", 0, 7000, text(0))    # same epoch as t00: never a pair
        self.rows = lens.prepare(ans)

    def test_exclusions_and_line(self):
        r = lens.independent_baseline(self.rows, sensitivity=())
        self.assertGreater(r["pairs_independent_sharing_a_gram"], 0)
        self.assertGreaterEqual(r["pairs_excluded_cluster_signals"], 1)  # the ring pair
        d = r["pair_dist"]
        self.assertLess(d["max"], 1.0)  # copies were kept out of the baseline
        expect = max(d["p99.5"] + r["margin"], d["p99.9"])
        self.assertAlmostEqual(r["line"], -(-round(expect * 100, 6) // 1) / 100.0)
        self.assertGreaterEqual(r["line"], d["p99.5"] + r["margin"] - 1e-9)

    def test_timing_group_excluded(self):
        base = lens.independent_baseline(self.rows, sensitivity=())
        tg = lens.independent_baseline(self.rows, timing_groups=[["acct0", "acct5", "acct10", "acct15"]], sensitivity=())
        self.assertLess(tg["pairs_independent_sharing_a_gram"], base["pairs_independent_sharing_a_gram"])

    def test_empty(self):
        r = lens.independent_baseline({}, sensitivity=())
        self.assertIsNone(r["line"])


class TestReviewLineWiring(unittest.TestCase):
    def test_build_doc_uses_line_and_renders(self):
        a = {"x": A("p", 1, 1, text(1)), "y": A("q", 2, 2, text(1))}
        idx = {k: {"from": v["from"], "epoch": v["epoch"], "ts": v["ts"]} for k, v in a.items()}
        rv = {"line": 0.99, "pair_dist": {k: 0.1 for k in ("p50", "p90", "p99", "p99.5", "p99.9", "max", "mean")},
              "per_answer_best_match": {"p50": 0.1, "p90": 0.1, "p99": 0.1, "p99.5": 0.1, "max": 0.1, "answers_at_or_above_line": 0},
              "segment": "s1", "carried_over": True, "shown_segment_answers_with_text": 0, "answers": 2, "authors": 2, "epochs": [1, 2],
              "pairs_diff_author_diff_epoch": 1, "pairs_excluded_cluster_signals": 0, "pairs_independent_sharing_a_gram": 1,
              "independent_pairs_at_or_above_line": 0, "exclude_link": 0.5, "percentile": 0.995, "margin": 0.1, "sensitivity": []}
        doc = render.build_doc(a, idx, [], {"entries": []}, 0, review=rv)
        self.assertEqual(doc["method"]["s1_threshold"], 0.99)
        self.assertEqual(len(doc["families"]), 1)  # identical texts (1.0) still link at 0.99
        h = render.render_html(doc)
        self.assertIn("id=review-line", h)
        self.assertIn("containment ≥ 0.99", h)
        self.assertIn("carried over", h)

    def test_review_line_carries_over_from_segment_with_text(self):
        bodies = {"t%d" % i: dict(A("a%d" % i, i % 3, i, text(i, shared=6 if i % 5 == 0 and i else 0, base=0)), seg="poa-mainnet-1-segment1") for i in range(lens.BASELINE_MIN_ANSWERS)}
        idx = {k: {"from": v["from"], "epoch": v["epoch"], "ts": v["ts"], "seg": v["seg"]} for k, v in bodies.items()}
        rv = build.review_line(bodies, idx, "poa-mainnet-1-segment2")
        self.assertIsNotNone(rv)
        self.assertEqual(rv["segment"], "poa-mainnet-1-segment1")
        self.assertTrue(rv["carried_over"])
        self.assertIsNone(build.review_line({}, {}, "poa-mainnet-1-segment2"))

    def test_fetch_limits_unchanged(self):
        import inspect
        import fetch
        sig = inspect.signature(fetch.fill_bodies)
        self.assertEqual((sig.parameters["max_errors"].default, sig.parameters["deadline_s"].default), (3, 300))
        self.assertIn('"CLUSTERS_BUDGET", "25"', open(os.path.join(os.path.dirname(build.__file__), "build.py")).read())


if __name__ == "__main__":
    unittest.main()


class TestReviewLineMinPairs(unittest.TestCase):
    def test_thin_sample_does_not_set_line(self):
        # enough answers, but only a handful of pairs share any 5-gram: below BASELINE_MIN_PAIRS, so no line
        bodies = {"t%d" % i: dict(A("a%d" % i, i % 3, i, text(i, shared=6 if i % 50 == 0 and i else 0, base=0)), seg="poa-mainnet-1-segment1") for i in range(lens.BASELINE_MIN_ANSWERS)}
        idx = {k: {"from": v["from"], "epoch": v["epoch"], "ts": v["ts"], "seg": v["seg"]} for k, v in bodies.items()}
        rows = lens.prepare(bodies)
        r = lens.independent_baseline(rows, sensitivity=())
        self.assertIsNotNone(r["line"])
        self.assertLess(r["pairs_independent_sharing_a_gram"], lens.BASELINE_MIN_PAIRS)
        self.assertIsNone(build.review_line(bodies, idx, "poa-mainnet-1-segment1"))
