"""Text has to be readable on every ground the app actually renders on.

This is not a theoretical standard here. The portal's readers are condo
owners, many of them elderly, reading dates and meta lines at 12–13px. A
muted grey that looks refined on a designer's monitor is the difference
between a legible archive and a squint.

WCAG AA asks 4.5:1 for text below 18.66px, which is every date, subtitle,
label and meta line in this app. The check runs against each brand ground
because a token can pass on white and fail on tinted paper.
"""
import re

from django.conf import settings
from django.test import SimpleTestCase

CSS = (settings.BASE_DIR / "static" / "portal.css").read_text()
FLOOR = 4.5


def _channel(value):
    value /= 255
    return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4


def luminance(hex_colour):
    hex_colour = hex_colour.lstrip("#")
    r, g, b = (int(hex_colour[i:i + 2], 16) for i in (0, 2, 4))
    return 0.2126 * _channel(r) + 0.7152 * _channel(g) + 0.0722 * _channel(b)


def contrast(foreground, background):
    a, b = luminance(foreground) + 0.05, luminance(background) + 0.05
    return round(max(a, b) / min(a, b), 2)


def light_tokens():
    """The :root block only — not the dark-mode overrides beneath it."""
    root = CSS[CSS.index(":root {"):CSS.index("@media (prefers-color-scheme: dark)")]
    return dict(re.findall(r"(--[a-z0-9-]+):\s*(#[0-9a-fA-F]{6})", root))


# The paper each property renders on. Kept here rather than read from compose
# so the check states what it is protecting; a new property adds a line.
BRAND_GROUNDS = {
    "shore club": "#faf7f2",
    "the sands": "#f6f3ec",
    "the palms": "#f4f5f1",
}
BRAND_ACCENTS = {
    "shore club": "#b04a37",
    "the sands": "#0d7572",
    "the palms": "#2f5d4a",
}


class TextContrastTests(SimpleTestCase):
    def grounds(self):
        tokens = light_tokens()
        surfaces = {"card": tokens["--surface"], "raised panel": tokens["--surface-2"]}
        return dict(BRAND_GROUNDS, **surfaces)

    def test_body_and_muted_text_pass_on_every_ground(self):
        tokens = light_tokens()
        failures = []
        for name in ("--ink", "--ink-2", "--ink-3"):
            for where, ground in self.grounds().items():
                ratio = contrast(tokens[name], ground)
                if ratio < FLOOR:
                    failures.append("%s (%s) on %s: %.2f:1" % (name, tokens[name], where, ratio))
        self.assertEqual(failures, [], "text below %s:1 — %s" % (FLOOR, failures))

    def test_each_accent_passes_on_its_own_paper(self):
        """An accent is used for links and small labels, not just buttons.

        Every one of these passed on white and failed on its own tinted
        ground, which is exactly the case a white-background check misses.
        """
        failures = []
        for brand, accent in BRAND_ACCENTS.items():
            ratio = contrast(accent, BRAND_GROUNDS[brand])
            if ratio < FLOOR:
                failures.append("%s accent %s on its ground: %.2f:1" % (brand, accent, ratio))
        self.assertEqual(failures, [], "accents below %s:1 — %s" % (FLOOR, failures))

    def test_white_on_accent_is_readable(self):
        """Buttons put white text on the accent — the other direction."""
        for brand, accent in BRAND_ACCENTS.items():
            self.assertGreaterEqual(contrast("#ffffff", accent), FLOOR, brand)

    def test_dark_mode_text_passes_on_dark_surfaces(self):
        dark = CSS[CSS.index("@media (prefers-color-scheme: dark)"):]
        tokens = dict(re.findall(r"(--[a-z0-9-]+):\s*(#[0-9a-fA-F]{6})", dark))
        for name in ("--ink", "--ink-2", "--ink-3"):
            for surface in ("--bg", "--surface", "--surface-2", "--panel"):
                ratio = contrast(tokens[name], tokens[surface])
                self.assertGreaterEqual(ratio, FLOOR, "%s on %s: %.2f:1" % (name, surface, ratio))


class RailPanelTests(SimpleTestCase):
    """The browse panel is barely a tint; the rule down its edge does the
    separating. A heavier panel eats the headroom the muted ink has, which is
    why the rail's faintest text is --ink-2 rather than --ink-3.
    """

    def panel_mix(self):
        root = CSS[CSS.index(":root {"):CSS.index("@media (prefers-color-scheme: dark)")]
        found = re.search(
            r"--panel:\s*color-mix\(in srgb, var\(--brand-ground\) (\d+)%, var\(--ink\)\)", root)
        self.assertIsNotNone(found, "the panel is no longer derived from the brand ground")
        return int(found.group(1)) / 100

    def rail_text_tokens(self):
        rail = CSS[CSS.index(".rail {"):CSS.index("\n.pane {")]
        return set(re.findall(r"color:\s*var\((--ink[a-z0-9-]*)\)", rail))

    def panel_for(self, ground_hex):
        tokens = light_tokens()
        mix = self.panel_mix()
        ink = [int(tokens["--ink"].lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
        ground = [int(ground_hex.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4)]
        return [round(mix * ground[i] + (1 - mix) * ink[i]) for i in range(3)]

    def test_the_panel_is_derived_from_the_property_not_hardcoded(self):
        self.assertGreater(self.panel_mix(), 0)
        self.assertIn("var(--panel)", CSS[CSS.index(".rail {"):CSS.index(".rail-head {")])

    def test_the_separation_comes_from_a_rule_not_from_weight(self):
        rail = CSS[CSS.index(".rail {"):CSS.index(".rail-inner {")]
        self.assertIn("border-right", rail)

    def test_the_rail_does_not_use_the_muted_ink_that_fails_on_it(self):
        self.assertNotIn("--ink-3", self.rail_text_tokens(),
                         "--ink-3 does not clear 4.5:1 under the watermark")


class RailWatermarkTests(SimpleTestCase):
    """The watermark is a real photograph, so the guarantee has to be checked
    on the FILES, not on the recipe that made them.

    The tones are squeezed into a narrow band before the image ever reaches the
    browser, and `multiply` means it can only ever darken the panel. JPEG is
    the trap: compression overshoots at edges and pushed the first build eleven
    levels below the intended floor, quietly breaching the margin. Hence a test
    that opens the shipped images.
    """

    DARKEST = 220  # the measured limit: below this the faintest label fails

    def images(self):
        folder = settings.BASE_DIR / "static" / "rail"
        return sorted(folder.glob("*.jpg")) + sorted(folder.glob("*.png"))

    def test_the_watermark_can_only_darken_the_panel(self):
        rail = CSS[CSS.index(".rail-inner {"):CSS.index(".rail-head {")]
        self.assertIn("background-blend-mode: multiply", rail,
                      "a mode that can lighten needs the contrast sum redone")

    def test_every_shipped_watermark_stays_inside_its_tonal_band(self):
        from PIL import Image
        found = self.images()
        self.assertTrue(found, "no watermark images shipped")
        for path in found:
            darkest, _ = Image.open(path).convert("L").getextrema()
            self.assertGreaterEqual(
                darkest, self.DARKEST,
                "%s dips to %d — the panel goes darker than the text was "
                "measured against" % (path.name, darkest))

    def test_the_faintest_rail_text_survives_the_darkest_watermark(self):
        tokens = light_tokens()
        panel_test = RailPanelTests()
        faintest = max(
            (tokens[t] for t in panel_test.rail_text_tokens()),
            key=luminance,   # the palest ink is the hardest case
        )
        failures = []
        for brand, ground in BRAND_GROUNDS.items():
            panel = panel_test.panel_for(ground)
            worst = "#%02x%02x%02x" % tuple(
                round(c * self.DARKEST / 255) for c in panel)
            ratio = contrast(faintest, worst)
            if ratio < FLOOR:
                failures.append("%s: %s on %s is %.2f:1" % (brand, faintest, worst, ratio))
        self.assertEqual(failures, [], "under the watermark — %s" % failures)

    def test_the_watermark_is_per_property_config(self):
        rail = CSS[CSS.index(".rail-inner {"):CSS.index(".rail-head {")]
        self.assertIn("var(--rail-image", rail)
        self.assertNotIn(".jpg", rail, "a property's watermark is hardcoded in the stylesheet")


class MastheadAccentTests(SimpleTestCase):
    """The property's full-strength colour, used where it can be.

    These are the resorts' own colours, taken from their sites: Sands' aqua and
    Shore Club's orange are footer backgrounds there. Neither can sit BEHIND
    white text — 2.6:1 — which is why the portal keeps a dark masthead and uses
    them on it instead. That only works while the masthead stays dark enough.
    """

    MASTHEADS = {"shore club": "#2f3133", "the sands": "#14383a", "the palms": "#1e2f27"}
    VIVID = {"shore club": "#ff7760", "the sands": "#00b3b0"}

    def test_each_vivid_colour_reads_on_its_own_masthead(self):
        failures = []
        for brand, vivid in self.VIVID.items():
            ratio = contrast(vivid, self.MASTHEADS[brand])
            if ratio < FLOOR:
                failures.append("%s %s on %s: %.2f:1" % (
                    brand, vivid, self.MASTHEADS[brand], ratio))
        self.assertEqual(failures, [], "vivid accent unreadable — %s" % failures)

    def test_white_still_reads_on_every_masthead(self):
        for brand, masthead in self.MASTHEADS.items():
            self.assertGreaterEqual(contrast("#ffffff", masthead), 7, brand)

    def test_the_vivid_colour_is_never_used_as_a_text_background(self):
        """A guard on the mistake this design exists to avoid."""
        for brand, vivid in self.VIVID.items():
            self.assertLess(contrast("#ffffff", vivid), FLOOR,
                            "%s: if this now passes, revisit the masthead decision" % brand)

    def test_the_masthead_rule_uses_the_vivid_token(self):
        header = CSS[CSS.index(".site-header {"):CSS.index(".header-inner {")]
        self.assertIn("border-bottom: 3px solid var(--vivid)", header)

    def test_the_header_height_accounts_for_that_rule(self):
        root = CSS[CSS.index(":root {"):CSS.index("@media (prefers-color-scheme: dark)")]
        found = re.search(r"--header-h:\s*calc\(max\(var\(--logo-h\), 40px\) \+ (\d+)px\)", root)
        self.assertIsNotNone(found)
        # 16px padding top and bottom, plus the 3px rule.
        self.assertEqual(int(found.group(1)), 35)
