from __future__ import annotations

import logging
import math
import os.path
import uuid

import pytest
from copy import deepcopy
from datetime import datetime
from typing import TYPE_CHECKING

from fontTools.misc.transform import Transform as Affine

from glyphsLib.classes import (
    GSAnchor,
    GSFont,
    GSFontMaster,
    GSGlyph,
    GSLayer,
    GSComponent,
    GSSmartComponentAxis,
)
from glyphsLib.glyphdata import get_glyph
from glyphsLib.types import Point, Transform
from glyphsLib.writer import dumps

from glyphsLib.builder.transformations.align_alternate_layers import (
    align_alternate_layers,
)
from glyphsLib.builder.transformations.propagate_anchors import (
    _get_design_space_info,
    compute_max_component_depths,
    get_xy_rotation,
    propagate_all_anchors,
    propagate_all_anchors_impl,
    depth_sorted_composite_glyphs,
)

if TYPE_CHECKING:
    from typing import Callable, Self

DATA = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "data")


# Basically all the tests below are translated from:
# https://github.com/googlefonts/fontc/blob/ecc727d/glyphs-reader/src/propagate_anchors.rs#L423-L959
# This is to ensure that the Python implementation behaves the same way as the Rust one.


class GlyphSetBuilder:
    glyphs: dict[str, GSGlyph]

    def __init__(self):
        self.font = GSFont()
        self.font.format_version = 3

    def build(self) -> dict[str, GSGlyph]:
        return {g.name: g for g in self.font.glyphs}

    def add_glyph(self, name: str, build_fn: Callable[["GlyphBuilder"], None]) -> Self:
        glyph = GlyphBuilder(name, self.font)
        build_fn(glyph)
        # this inserts the glyph in the font and sets the glyph.parent attribute to it;
        # GSLayer._is_bracket_layer()/_bracket_axis_rules() require this to check the
        # glyph.parent.format_version to determine where to look for the axis rules
        self.font.glyphs.append(glyph.build())
        return self


class GlyphBuilder:
    def __init__(self, name: str, font: GSFont = None):
        info = get_glyph(name)
        self.glyph = glyph = GSGlyph()
        glyph.name = name
        glyph.unicode = info.unicode
        glyph.category = info.category
        glyph.subCategory = info.subCategory
        glyph.parent = font
        self.num_masters = 0
        self.num_alternates = 0
        self.add_master_layer()

    def build(self) -> GSGlyph:
        return self.glyph

    def add_master_layer(self) -> Self:
        layer = GSLayer()
        layer.name = layer.layerId = layer.associatedMasterId = (
            f"master-{self.num_masters}"
        )
        self.num_masters += 1
        self.num_alternates = 0
        self.glyph.layers.append(layer)
        self.current_layer = layer
        return self

    def add_backup_layer(self, associated_master_idx=0):
        layer = GSLayer()
        layer.name = datetime.now().isoformat()
        layer.layerId = str(uuid.uuid4()).upper()
        master_layer = self.glyph.layers[associated_master_idx]
        layer.associatedMasterId = master_layer.layerId
        self.glyph.layers.append(layer)
        self.current_layer = layer
        return self

    def add_bracket_layer(self, axis_rules):
        assert self.current_layer._is_master_layer
        associated_master_id = self.current_layer.layerId
        master_layer = self.glyph.layers[associated_master_id]
        layer = GSLayer()
        layer.name = f"{master_layer.name} bracket-{self.num_alternates}"
        layer.layerId = str(uuid.uuid4()).upper()
        layer.associatedMasterId = associated_master_id
        layer.attributes["axisRules"] = axis_rules
        self.num_alternates += 1
        self.glyph.layers.append(layer)
        self.current_layer = layer
        return self

    def add_brace_layer(self, coordinates, associated_master_idx=None):
        if associated_master_idx is None:
            # Find the most recent master layer
            associated_master_id = None
            for l in reversed(list(self.glyph.layers)):
                if l._is_master_layer:
                    associated_master_id = l.layerId
                    break
            assert associated_master_id is not None
        else:
            master_layer = self.glyph.layers[associated_master_idx]
            associated_master_id = master_layer.layerId
        layer = GSLayer()
        layer.name = f"{{{', '.join(str(c) for c in coordinates)}}}"
        layer.layerId = str(uuid.uuid4()).upper()
        layer.associatedMasterId = associated_master_id
        layer.attributes["coordinates"] = coordinates
        self.glyph.layers.append(layer)
        self.current_layer = layer
        return self

    def set_category(self, category: str) -> Self:
        self.glyph.category = category
        return self

    def set_subCategory(self, subCategory: str) -> Self:
        self.glyph.subCategory = subCategory
        return self

    def add_smartComponentAxis(
        self, name: str, top_value: float, bottom_value: float
    ) -> Self:
        axis = GSSmartComponentAxis()
        axis.name = name
        axis.topValue = top_value
        axis.bottomValue = bottom_value
        self.glyph.smartComponentAxes.append(axis)
        return self

    def set_smartComponentPoleMapping(self, name: str, pole: int) -> Self:
        self.current_layer.smartComponentPoleMapping[name] = pole
        return self

    def add_component(
        self,
        name: str,
        pos: tuple[float, float],
        smart_component_values: dict[str, float] = None,
    ) -> Self:
        component = GSComponent(name, offset=pos)
        self.current_layer.components.append(component)
        if smart_component_values:
            component.smartComponentValues = smart_component_values
        return self

    def rotate_component(self, degrees: float) -> Self:
        # Set an explicit translate + rotation for the component
        component = self.current_layer.components[-1]
        component.transform = Transform(
            *Affine(*component.transform).rotate(math.radians(degrees))
        )
        return self

    def add_component_anchor(self, name: str) -> Self:
        # add an explicit anchor to the last added component
        component = self.current_layer.components[-1]
        component.anchor = name
        return self

    def add_anchor(
        self,
        name: str,
        pos: tuple[float, float],
        userData: dict | None = None,
    ) -> Self:
        anchor = GSAnchor(name, Point(*pos), userData=userData)
        self.current_layer.anchors.append(anchor)
        return self


def make_glyph(name: str, components: list[str]) -> GSGlyph:
    builder = GlyphBuilder(name)
    for comp in components:
        builder.add_component(comp, (0, 0))  # pos doesn't matter for this test
    return builder.build()


def test_components_by_depth():
    glyphs = {
        name: make_glyph(name, components)
        for name, components in [
            ("A", []),
            ("E", []),
            ("acutecomb", []),
            ("brevecomb", []),
            ("brevecomb_acutecomb", ["acutecomb", "brevecomb"]),
            ("AE", ["A", "E"]),
            ("Aacute", ["A", "acutecomb"]),
            ("Aacutebreve", ["A", "brevecomb_acutecomb"]),
            ("AEacutebreve", ["AE", "brevecomb_acutecomb"]),
            ("acute", ["acutecomb.case"]),
            ("acutecomb.case", ["acutecomb.alt"]),
            ("acutecomb.alt", ["acute"]),
            ("grave", ["grave"]),
        ]
    }

    assert compute_max_component_depths(glyphs) == {
        "A": 0,
        "E": 0,
        "acutecomb": 0,
        "brevecomb": 0,
        "brevecomb_acutecomb": 1,
        "AE": 1,
        "Aacute": 1,
        "Aacutebreve": 2,
        "AEacutebreve": 2,
        "acute": float("inf"),
        "acutecomb.case": float("inf"),
        "acutecomb.alt": float("inf"),
        "grave": float("inf"),
    }

    assert depth_sorted_composite_glyphs(glyphs) == [
        "A",
        "E",
        "acutecomb",
        "brevecomb",
        "AE",
        "Aacute",
        "brevecomb_acutecomb",
        "AEacutebreve",
        "Aacutebreve",
        # cyclical composites are skipped
    ]


def assert_equal_gsobjects(object1, object2):
    # glyphsLib.classes objects don't implement __eq__, so we resort to compare
    # their serialized forms... Ugly but works :(
    assert dumps(object1) == dumps(object2)


def assert_equal_glyphsets(glyphs1, glyphs2):
    assert len(glyphs1) == len(glyphs2)
    assert glyphs1.keys() == glyphs2.keys()
    for name in glyphs1:
        assert_equal_gsobjects(glyphs1[name], glyphs2[name])


def assert_anchors(actual, expected):
    assert len(actual) == len(expected)
    for a, e in zip(actual, expected):
        assert a.name == e[0]
        assert a.position == Point(*e[1])
        assert dict(a.userData) == (e[2] if len(e) > 2 else {})


def test_no_components_anchors_are_unchanged():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "A",
            lambda glyph: (
                glyph.add_anchor("bottom", (234, 0))
                .add_anchor("ogonek", (411, 0))
                .add_anchor("top", (234, 810))
            ),
        )
        .add_glyph(
            "acutecomb",
            lambda glyph: (
                glyph.add_anchor("_top", (0, 578)).add_anchor("top", (0, 810))
            ),
        )
        .build()
    )

    glyphs2 = deepcopy(glyphs)
    propagate_all_anchors_impl(glyphs2)
    # nothing should change here
    assert_equal_glyphsets(glyphs, glyphs2)


def test_basic_composite_anchor():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "A",
            lambda glyph: (
                glyph.add_anchor("bottom", (234, 0))
                .add_anchor("ogonek", (411, 0))
                .add_anchor("top", (234, 810))
            ),
        )
        .add_glyph(
            "acutecomb",
            lambda glyph: (
                glyph.add_anchor("_top", (0, 578)).add_anchor("top", (0, 810))
            ),
        )
        .add_glyph(
            "Aacute",
            lambda glyph: (
                glyph.add_component("A", (0, 0)).add_component("acutecomb", (234, 232))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["Aacute"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("bottom", (234, 0)),
            ("ogonek", (411, 0)),
            ("top", (234, 1042)),
        ],
    )


def test_propagate_ligature_anchors():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    # this is based on the IJ glyph in Oswald (ExtraLight)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "I",
            lambda glyph: (
                glyph.add_anchor("bottom", (103, 0))
                .add_anchor("ogonek", (103, 0))
                .add_anchor("top", (103, 810))
                .add_anchor("topleft", (20, 810))
            ),
        )
        .add_glyph(
            "J",
            lambda glyph: (
                glyph.add_anchor("bottom", (133, 0)).add_anchor("top", (163, 810))
            ),
        )
        .add_glyph(
            "IJ",
            lambda glyph: (
                glyph.set_subCategory("Ligature")
                .add_component("I", (0, 0))
                .add_component("J", (206, 0))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)
    ij = glyphs["IJ"]
    # these were derived by running the built in glyphs.app propagate anchors
    # method from the macro panel
    assert_anchors(
        ij.layers[0].anchors,
        [
            ("bottom_1", (103, 0)),
            ("ogonek_1", (103, 0)),
            ("top_1", (103, 810)),
            ("topleft_1", (20, 810)),
            ("bottom_2", (339, 0)),
            ("top_2", (369, 810)),
        ],
    )


def test_digraphs_arent_ligatures():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    # this is based on the IJ glyph in Oswald (ExtraLight)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "I",
            lambda glyph: (
                glyph.add_anchor("bottom", (103, 0))
                .add_anchor("ogonek", (103, 0))
                .add_anchor("top", (103, 810))
                .add_anchor("topleft", (20, 810))
            ),
        )
        .add_glyph(
            "J",
            lambda glyph: (
                glyph.add_anchor("bottom", (133, 0)).add_anchor("top", (163, 810))
            ),
        )
        .add_glyph(
            "IJ",
            lambda glyph: (
                glyph.add_component("I", (0, 0)).add_component("J", (206, 0))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)
    ij = glyphs["IJ"]
    # these were derived by running the built in glyphs.app propagate anchors
    # method from the macro panel
    assert_anchors(
        ij.layers[0].anchors,
        # 'J' component comes last; the 'bottom' and 'top' anchors are from 'J'
        # shifted by the 'J' component's offset (206, 0).
        # 'ogonek' and 'topleft' are inherited from 'I', the first component, which
        # has (0, 0) offset hence the same anchor positions as the original 'I' glyph.
        [
            ("bottom", (339, 0)),
            ("ogonek", (103, 0)),
            ("top", (369, 810)),
            ("topleft", (20, 810)),
        ],
    )


def test_propagate_across_layers(caplog):
    # derived from the observed behaviour of glyphs 3.4 (3414)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "A",
            lambda glyph: (
                glyph.add_anchor("bottom", (206, 16))
                .add_anchor("ogonek", (360, 13))
                .add_anchor("top", (212, 724))
                .add_bracket_layer([{"min": 500}])
                .add_anchor("bottom", (206, 16))
                .add_anchor("top", (212, 724))
                .add_master_layer()
                .add_anchor("bottom", (278, 12))
                .add_anchor("ogonek", (464, 13))
                .add_anchor("top", (281, 758))
                .add_bracket_layer([{"min": 500}])
                .add_anchor("bottom", (278, 12))
                .add_anchor("top", (281, 758))
                .add_backup_layer()
                .add_anchor("top", (290, 690))
            ),
        )
        .add_glyph(
            "acutecomb",
            lambda glyph: (
                glyph.add_anchor("_top", (150, 580))
                .add_anchor("top", (170, 792))
                .add_master_layer()
                .add_anchor("_top", (167, 580))
                .add_anchor("top", (170, 792))
                .add_backup_layer()
                .add_anchor("_top", (335, 502))
            ),
        )
        .add_glyph(
            "Aacute",
            lambda glyph: (
                glyph.add_component("A", (0, 0))
                .add_component("acutecomb", (62, 144))
                .add_bracket_layer([{"min": 500}])
                .add_component("A", (20, 0))
                .add_component("acutecomb", (82, 144))
                .add_master_layer()
                .add_component("A", (0, 0))
                .add_component("acutecomb", (114, 178))
                .add_bracket_layer([{"min": 500}])
                .add_component("A", (30, 0))
                .add_component("acutecomb", (144, 178))
                .add_backup_layer()
                .add_component("A", (0, 0))
                .add_component("acutecomb", (-45, 188))
            ),
        )
        .add_glyph(
            "acutecomb.case",
            lambda glyph: (
                glyph.add_component("acutecomb", (0, 0))
                .add_master_layer()
                .add_component("acutecomb", (0, 0))
                # this backup layer contains a potentially cyclical reference
                # as the component's base glyph in turn points back at self;
                # this doesn't trigger an infinite loop because backup layers
                # are skipped when propagating anchors
                .add_backup_layer()
                .add_component("acute", (0, 0))
            ),
        )
        .add_glyph(
            "acute",
            lambda glyph: (
                glyph.add_component("acutecomb.case", (0, 0))
                .add_master_layer()
                .add_component("acutecomb.case", (0, 0))
            ),
        )
        .build()
    )
    with caplog.at_level(logging.WARNING):
        propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["Aacute"]
    assert_anchors(
        new_glyph.layers["master-0"].anchors,
        [
            ("bottom", (206, 16)),
            ("ogonek", (360, 13)),
            ("top", (232, 936)),
        ],
    )

    assert_anchors(
        new_glyph.layers["master-1"].anchors,
        [
            ("bottom", (278, 12)),
            ("ogonek", (464, 13)),
            ("top", (284, 970)),
        ],
    )

    # alternate 'bracket' layers should work as well
    alternate_layers = [l for l in new_glyph.layers if l._is_bracket_layer()]
    assert len(alternate_layers) == 2
    assert_anchors(
        alternate_layers[0].anchors,
        [
            ("bottom", (226, 16)),
            ("top", (252, 936)),
        ],
    )
    assert_anchors(
        alternate_layers[1].anchors,
        [
            ("bottom", (308, 12)),
            ("top", (314, 970)),
        ],
    )

    # non-master (e.g. backup) layers are silently skipped
    assert not new_glyph.layers[-1]._is_master_layer
    assert not new_glyph.layers[-1]._is_bracket_layer()
    assert_anchors(new_glyph.layers[-1].anchors, [])

    assert len(caplog.records) == 0


def test_propagate_across_layers_with_circular_reference(caplog):
    glyphs = (
        GlyphSetBuilder()
        # acutecomb.alt contains a cyclical reference to itself in its master layer
        # test that this doesn't cause an infinite loop
        .add_glyph(
            "acutecomb.alt",
            lambda glyph: (
                glyph.add_component("acutecomb.alt", (0, 0))
                .add_master_layer()
                .add_component("acutecomb.alt", (0, 0))
            ),
        )
        # gravecomb and grave contain cyclical component references to one another
        # in their master layers; test that this doesn't cause an infinite loop either
        .add_glyph(
            "gravecomb",
            lambda glyph: (
                glyph.add_component("grave", (0, 0))
                .add_master_layer()
                .add_component("grave", (0, 0))
            ),
        )
        .add_glyph(
            "grave",
            lambda glyph: (
                glyph.add_component("gravecomb", (0, 0))
                .add_master_layer()
                .add_component("gravecomb", (0, 0))
            ),
        )
        .build()
    )

    with caplog.at_level(logging.WARNING):
        propagate_all_anchors_impl(glyphs)

    assert len(caplog.records) == 2
    assert caplog.records[0].message == "glyph 'acutecomb.alt' has cyclical components"
    assert caplog.records[1].message == "glyph 'gravecomb' has cyclical components"


def test_remove_exit_anchor_on_component():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph("comma", lambda glyph: ())
        .add_glyph(
            "ain-ar.init",
            lambda glyph: (
                glyph.add_anchor("top", (294, 514)).add_anchor("exit", (0, 0))
            ),
        )
        .add_glyph(
            "ain-ar.init.alt",
            lambda glyph: (
                glyph.add_component("ain-ar.init", (0, 0)).add_component(
                    "comma", (0, 0)
                )
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["ain-ar.init.alt"]
    assert_anchors(new_glyph.layers[0].anchors, [("top", (294, 514))])


def test_component_anchor():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "acutecomb",
            lambda glyph: (
                glyph.add_anchor("_top", (150, 580)).add_anchor("top", (170, 792))
            ),
        )
        .add_glyph(
            "aa",
            lambda glyph: (
                glyph.add_anchor("bottom_1", (218, 8))
                .add_anchor("bottom_2", (742, 7))
                .add_anchor("ogonek_1", (398, 9))
                .add_anchor("ogonek_2", (902, 9))
                .add_anchor("top_1", (227, 548))
                .add_anchor("top_2", (746, 548))
            ),
        )
        .add_glyph(
            "a_a",
            lambda glyph: glyph.add_component("aa", (0, 0)),
        )
        .add_glyph(
            "a_aacute",
            lambda glyph: (
                glyph.add_component("a_a", (0, 0))
                .add_component("acutecomb", (596, -32))
                .add_component_anchor("top_2")
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["a_aacute"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("bottom_1", (218, 8)),
            ("bottom_2", (742, 7)),
            ("ogonek_1", (398, 9)),
            ("ogonek_2", (902, 9)),
            ("top_1", (227, 548)),
            ("top_2", (766, 760)),
        ],
    )


def test_origin_anchor():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "a",
            lambda glyph: (
                glyph.add_anchor("*origin", (-20, 0))
                .add_anchor("bottom", (242, 7))
                .add_anchor("ogonek", (402, 9))
                .add_anchor("top", (246, 548))
            ),
        )
        .add_glyph(
            "acutecomb",
            lambda glyph: (
                glyph.add_anchor("_top", (150, 580)).add_anchor("top", (170, 792))
            ),
        )
        .add_glyph(
            "aacute",
            lambda glyph: (
                glyph.add_component("a", (0, 0)).add_component("acutecomb", (116, -32))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["aacute"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("bottom", (262, 7)),
            ("ogonek", (422, 9)),
            ("top", (286, 760)),
        ],
    )


def test_contextual_anchors():
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "behDotless-ar.init",
            lambda glyph: (
                glyph.add_anchor("bottom", (50, 0))
                .add_anchor(
                    "*bottom",
                    (95, 0),
                    userData={"GPOS_Context": "* behDotless-ar.medi"},
                )
                .add_anchor("top", (35, 229))
            ),
        )
        .add_glyph(
            "behDotless-ar.medi",
            lambda glyph: (
                glyph.add_component("behDotless-ar.init", (0, 0)).add_anchor(
                    "*bottom",
                    (95, 0),
                    userData={"GPOS_Context": "* behDotless-ar.fina"},
                )
            ),
        )
        .add_glyph(
            "behDotless-ar.fina",
            lambda glyph: glyph.add_component("behDotless-ar.init", (0, 0)),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["behDotless-ar.medi"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("bottom", (50, 0)),
            ("*bottom", (95, 0), {"GPOS_Context": "* behDotless-ar.fina"}),
            ("top", (35, 229)),
        ],
    )

    new_glyph = glyphs["behDotless-ar.fina"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("bottom", (50, 0)),
            ("*bottom", (95, 0), {"GPOS_Context": "* behDotless-ar.medi"}),
            ("top", (35, 229)),
        ],
    )


def test_invert_names_on_rotation():
    # derived from the observed behaviour of glyphs 3.2.2 (3259)
    glyphs = (
        GlyphSetBuilder()
        .add_glyph("comma", lambda glyph: ())
        .add_glyph(
            "commaaccentcomb",
            lambda glyph: (
                glyph.add_anchor("_bottom", (289, 0))
                .add_anchor("mybottom", (277, -308))
                .add_component("comma", (9, -164))
            ),
        )
        .add_glyph(
            "commaturnedabovecomb",
            lambda glyph: (
                glyph.add_component("commaaccentcomb", (589, 502)).rotate_component(180)
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["commaturnedabovecomb"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [("_top", (300, 502)), ("mytop", (312, 810))],
    )


def test_affine_scale():
    assert get_xy_rotation(Affine().translate(589, 502).rotate(math.radians(180))) == (
        -1,
        -1,
    )
    assert get_xy_rotation(Affine().translate(10, 10)) == (1, 1)
    assert get_xy_rotation(Affine().scale(1, -1)) == (1, -1)
    assert get_xy_rotation(Affine().scale(-1, 1)) == (-1, 1)
    assert get_xy_rotation(
        Affine().translate(589, 502).rotate(math.radians(180)).scale(-1, 1)
    ) == (
        1,
        -1,
    )


def test_real_files():
    # the tricky parts of these files have been factored out into separate tests,
    # but we'll keep them in case there are other regressions lurking
    expected = GSFont(os.path.join(DATA, "PropagateAnchorsTest-propagated.glyphs"))
    font = GSFont(os.path.join(DATA, "PropagateAnchorsTest.glyphs"))

    propagate_all_anchors(font)

    assert len(font.glyphs) == len(expected.glyphs)
    assert [g.name for g in font.glyphs] == [g.name for g in expected.glyphs]
    for g1, g2 in zip(font.glyphs, expected.glyphs):
        assert len(g1.layers) == len(g2.layers)
        for l1, l2 in zip(g1.layers, g2.layers):
            # seems like the latest Glyphs.app insists on sorting anchors alphabetically
            # whereas we keep the original order; but what matters is their name and
            # position
            assert sorted((a.name, tuple(a.position)) for a in l1.anchors) == sorted(
                (a.name, tuple(a.position)) for a in l2.anchors
            )


@pytest.mark.parametrize(
    "test_file",
    [
        "AlignAlternateLayers-g2.glyphs",
        "AlignAlternateLayers-g3.glyphs",
    ],
)
def test_propagate_anchors_after_aligining_alternates(test_file):
    # https://github.com/googlefonts/glyphsLib/issues/1090
    font = GSFont(os.path.join(DATA, test_file))

    Cacute = font.glyphs["Cacute"]
    assert not any(l._is_bracket_layer() for l in Cacute.layers)
    for layer in Cacute.layers:
        assert len(layer.anchors) == 0

    align_alternate_layers(font)
    propagate_all_anchors(font)

    # all layers have now 2 anchors, including the new alternates
    Cacute = font.glyphs["Cacute"]
    alternate_layers = [l for l in Cacute.layers if l._is_bracket_layer()]
    assert len(alternate_layers) == 3
    for layer in Cacute.layers:
        assert len(layer.anchors) == 2
        assert [a.name for a in layer.anchors] == ["bottom", "top"]

    # the 'bottom' anchor in one of the alternate layers has different
    # position than the corresponding anchor in the associated master layer
    # (i.e. it was propagated and not simply duplicated)
    alt_layer = alternate_layers[0]
    alt_bottom = alt_layer.anchors[0]
    master_layer = Cacute.layers[alt_layer.associatedMasterId]
    master_bottom = master_layer.anchors[0]
    assert alt_bottom.name == master_bottom.name == "bottom"
    assert tuple(alt_bottom.position) == (329, 0)
    assert tuple(master_bottom.position) == (301, 0)


def test_entry_anchor_on_non_first_component():
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "part1",
            lambda glyph: (glyph.add_anchor("top", (10, 0))),
        )
        .add_glyph(
            "part2",
            lambda glyph: (glyph.add_anchor("entry.2", (10, 0))),
        )
        .add_glyph(
            "combo",
            lambda glyph: (
                glyph.add_component("part1", (0, 0)).add_component("part2", (100, 0))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["combo"]
    assert_anchors(new_glyph.layers[0].anchors, [("top", (10, 0))])


def test_cursive_anchors_ligature():
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "part1_part2",
            lambda glyph: (
                glyph.add_anchor("entry.1", (10, 0)).add_anchor("exit.1", (100, 0))
            ),
        )
        .add_glyph(
            "combo",
            lambda glyph: (
                glyph.set_subCategory("Ligature").add_component("part1_part2", (0, 0))
            ),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    new_glyph = glyphs["combo"]
    assert_anchors(
        new_glyph.layers[0].anchors,
        [
            ("entry.1", (10, 0)),
            ("exit.1", (100, 0)),
        ],
    )


def test_smart_component_anchors():
    glyphs = (
        GlyphSetBuilder()
        .add_glyph(
            "base",
            lambda glyph: (
                glyph.add_smartComponentAxis("TEST", 100, -100)
                .set_smartComponentPoleMapping("TEST", 1)
                .add_anchor("top", (23, 103))
                .add_anchor("bottom", (36, -51))
                .add_backup_layer()
                .set_smartComponentPoleMapping("TEST", 2)
                .add_anchor("top", (33, 123))
                .add_anchor("bottom", (36, -51))
            ),
        )
        .add_glyph(
            "smart1",
            lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": -100})),
        )
        .add_glyph(
            "smart2",
            lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": 100})),
        )
        .add_glyph(
            "smart3",
            lambda glyph: (glyph.add_component("base", (0, 0), {"TEST": 0})),
        )
        .add_glyph(
            "smart4",
            lambda glyph: (glyph.add_component("base", (20, 10), {"TEST": 0})),
        )
        .build()
    )
    propagate_all_anchors_impl(glyphs)

    assert_anchors(
        glyphs["smart1"].layers[0].anchors,
        [
            ("top", (23, 103)),
            ("bottom", (36, -51)),
        ],
    )
    assert_anchors(
        glyphs["smart2"].layers[0].anchors,
        [
            ("top", (33, 123)),
            ("bottom", (36, -51)),
        ],
    )
    assert_anchors(
        glyphs["smart3"].layers[0].anchors,
        [
            ("top", (28, 113)),
            ("bottom", (36, -51)),
        ],
    )
    assert_anchors(
        glyphs["smart4"].layers[0].anchors,
        [
            ("top", (48, 123)),
            ("bottom", (56, -41)),
        ],
    )


def test_bracket_ligature_anchor_numbering():
    """Ligature anchor numbering must work on synthesized bracket layers.

    When align_alternate_layers creates a bracket layer on a ligature composite
    (because its component has bracket layers), the synthesized layer has a
    different layerId than the component's bracket layer. The base_glyph_counts
    lookup must handle this mismatch, or ligature numbering is skipped.
    """
    axis_rules = [{"min": 160, "max": None}]

    builder = GlyphSetBuilder()
    # Add a master so align_alternate_layers can find master IDs
    master = GSFontMaster()
    master.id = "master-0"
    builder.font.masters.append(master)
    # "f" - simple base glyph with a top anchor
    builder.add_glyph(
        "f",
        lambda glyph: glyph.add_anchor("top", (100, 700)),
    )
    # "h" - base glyph with a bracket layer (different top anchor position)
    builder.add_glyph(
        "h",
        lambda glyph: (
            glyph.add_anchor("top", (200, 700))
            .add_bracket_layer(axis_rules)
            .add_anchor("top", (200, 600))
        ),
    )
    # "f_h" - ligature composite of f + h (no bracket layers of its own)
    builder.add_glyph(
        "f_h",
        lambda glyph: (
            glyph.set_subCategory("Ligature")
            .add_component("f", (0, 0))
            .add_component("h", (300, 0))
        ),
    )

    glyphs = builder.build()

    # align_alternate_layers synthesizes a bracket layer on f_h
    align_alternate_layers(builder.font)
    propagate_all_anchors_impl(glyphs)

    f_h = glyphs["f_h"]
    master_layers = [l for l in f_h.layers if l._is_master_layer]
    bracket_layers = [l for l in f_h.layers if l._is_bracket_layer()]

    assert len(bracket_layers) == 1

    # Master layer should have ligature-numbered anchors
    assert_anchors(
        master_layers[0].anchors,
        [("top_1", (100, 700)), ("top_2", (500, 700))],
    )
    # Bracket layer must also have ligature-numbered anchors (not plain "top")
    assert_anchors(
        bracket_layers[0].anchors,
        [("top_1", (100, 700)), ("top_2", (500, 600))],
    )


def test_get_design_space_info_none():
    """Returns empty dicts when font is None or has no masters."""
    assert _get_design_space_info(None) == ({}, {})
    assert _get_design_space_info(GSFont()) == ({}, {})


def test_get_design_space_info_single_axis():
    """Computes correct locations and triples for a single-axis font."""
    font = GSFont()
    font.format_version = 3
    master0 = GSFontMaster()
    master0.id = "light"
    master0.axes = [100]
    font.masters.append(master0)
    master1 = GSFontMaster()
    master1.id = "bold"
    master1.axes = [200]
    font.masters.append(master1)

    master_locations, axes_triples = _get_design_space_info(font)

    assert master_locations["light"]["Weight"] == 100
    assert master_locations["bold"]["Weight"] == 200
    # Default is first master (light), so triple is (min=100, default=100, max=200)
    assert axes_triples["Weight"] == (100, 100, 200)


def test_get_design_space_info_two_axes():
    """Computes correct locations and triples for a two-axis font."""
    font = GSFont()
    font.format_version = 3
    m0 = GSFontMaster()
    m0.id = "light-narrow"
    m0.axes = [100, 75]
    font.masters.append(m0)
    m1 = GSFontMaster()
    m1.id = "bold-narrow"
    m1.axes = [200, 75]
    font.masters.append(m1)
    m2 = GSFontMaster()
    m2.id = "bold-wide"
    m2.axes = [200, 100]
    font.masters.append(m2)

    master_locations, axes_triples = _get_design_space_info(font)

    assert len(master_locations) == 3
    assert master_locations["light-narrow"]["Weight"] == 100
    assert master_locations["light-narrow"]["Width"] == 75
    assert master_locations["bold-wide"]["Weight"] == 200
    assert master_locations["bold-wide"]["Width"] == 100
    # Default is first master
    assert axes_triples["Weight"] == (100, 100, 200)
    assert axes_triples["Width"] == (75, 75, 100)


def test_interpolate_brace_layer_component_anchors():
    """When a composite has a brace layer but its component doesn't, interpolate
    the component's anchor positions from its available master sources.

    See: https://github.com/googlefonts/fontc/issues/1661
    """
    builder = GlyphSetBuilder()
    # Set up font with two masters on the weight axis
    master0 = GSFontMaster()
    master0.id = "master-0"
    master0.axes = [100]  # light
    builder.font.masters.append(master0)
    master1 = GSFontMaster()
    master1.id = "master-1"
    master1.axes = [200]  # bold
    builder.font.masters.append(master1)

    # 'ae': base glyph with anchors at both masters
    builder.add_glyph(
        "ae",
        lambda glyph: (
            glyph.add_anchor("top", (300, 600))
            .add_anchor("bottom", (300, 0))
            .add_master_layer()
            .add_anchor("top", (380, 700))
            .add_anchor("bottom", (380, 0))
        ),
    )

    # 'acutecomb': mark glyph with anchors at both masters only (no brace layer)
    builder.add_glyph(
        "acutecomb",
        lambda glyph: (
            glyph.add_anchor("_top", (100, 500))
            .add_anchor("top", (120, 700))
            .add_master_layer()
            .add_anchor("_top", (140, 520))
            .add_anchor("top", (160, 740))
        ),
    )

    # 'aeacute': composite with 2 masters + a brace layer at weight=150
    builder.add_glyph(
        "aeacute",
        lambda glyph: (
            glyph.add_component("ae", (0, 0))
            .add_component("acutecomb", (200, 100))
            .add_master_layer()
            .add_component("ae", (0, 0))
            .add_component("acutecomb", (240, 120))
            # Add brace layer at weight=150 (midpoint)
            .add_brace_layer([150], associated_master_idx=0)
            .add_component("ae", (0, 0))
            .add_component("acutecomb", (220, 110))
        ),
    )

    propagate_all_anchors(builder.font)

    aeacute = builder.font.glyphs["aeacute"]

    # Master 0 (weight=100): acutecomb has explicit sources
    # top from acutecomb.top (120, 700) + offset (200, 100) = (320, 800)
    # bottom from ae: (300, 0)
    assert_anchors(
        aeacute.layers["master-0"].anchors,
        [("top", (320, 800)), ("bottom", (300, 0))],
    )

    # Master 1 (weight=200): acutecomb has explicit sources
    # top from acutecomb.top (160, 740) + offset (240, 120) = (400, 860)
    # bottom from ae: (380, 0)
    assert_anchors(
        aeacute.layers["master-1"].anchors,
        [("top", (400, 860)), ("bottom", (380, 0))],
    )

    # Brace layer (weight=150): both ae and acutecomb must be INTERPOLATED
    # Interpolated ae anchors at weight=150 (midpoint of 100..200):
    #   top: (300, 600) + 0.5 * ((380, 700) - (300, 600)) = (340, 650)
    #   bottom: (300, 0) + 0.5 * ((380, 0) - (300, 0)) = (340, 0)
    # Interpolated acutecomb anchors at weight=150:
    #   _top: (100, 500) + 0.5 * ((140, 520) - (100, 500)) = (120, 510)
    #   top:  (120, 700) + 0.5 * ((160, 740) - (120, 700)) = (140, 720)
    # Component offsets at brace layer: acutecomb offset = (220, 110)
    # Final top = interpolated acutecomb.top + offset
    #           = (140, 720) + (220, 110) = (360, 830)
    # Final bottom = interpolated ae.bottom = (340, 0)
    brace_layers = [l for l in aeacute.layers if l._is_brace_layer()]
    assert len(brace_layers) == 1
    assert_anchors(
        brace_layers[0].anchors,
        [("top", (360, 830)), ("bottom", (340, 0))],
    )


def test_brace_layer_matching():
    """When the component has a matching brace layer (same coordinates +
    associated master), use it directly without interpolation."""
    builder = GlyphSetBuilder()
    master0 = GSFontMaster()
    master0.id = "master-0"
    master0.axes = [100]
    builder.font.masters.append(master0)
    master1 = GSFontMaster()
    master1.id = "master-1"
    master1.axes = [200]
    builder.font.masters.append(master1)

    # 'base': has a brace layer at weight=150
    builder.add_glyph(
        "base",
        lambda glyph: (
            glyph.add_anchor("top", (100, 600))
            .add_master_layer()
            .add_anchor("top", (200, 700))
            .add_brace_layer([150], associated_master_idx=0)
            .add_anchor("top", (160, 660))  # explicit, not interpolated
        ),
    )

    # 'composite': also has a brace layer at weight=150
    builder.add_glyph(
        "composite",
        lambda glyph: (
            glyph.add_component("base", (0, 0))
            .add_master_layer()
            .add_component("base", (0, 0))
            .add_brace_layer([150], associated_master_idx=0)
            .add_component("base", (10, 0))
        ),
    )

    propagate_all_anchors(builder.font)

    composite = builder.font.glyphs["composite"]
    brace_layers = [l for l in composite.layers if l._is_brace_layer()]
    assert len(brace_layers) == 1
    # Should use the component's explicit brace layer anchor (160, 660) + offset (10, 0)
    assert_anchors(brace_layers[0].anchors, [("top", (170, 660))])


def test_brace_layer_interpolation_with_existing_brace_sources():
    """Component has brace layers at different coordinates; interpolation uses
    both masters and existing brace layers as sources."""
    builder = GlyphSetBuilder()
    master0 = GSFontMaster()
    master0.id = "master-0"
    master0.axes = [100]
    builder.font.masters.append(master0)
    master1 = GSFontMaster()
    master1.id = "master-1"
    master1.axes = [300]
    builder.font.masters.append(master1)

    # 'base': 2 masters + a brace layer at weight=200
    builder.add_glyph(
        "base",
        lambda glyph: (
            glyph.add_anchor("top", (100, 600))
            .add_master_layer()
            .add_anchor("top", (300, 800))
            .add_brace_layer([200], associated_master_idx=0)
            .add_anchor("top", (220, 720))  # off the linear path
        ),
    )

    # 'composite': brace layer at weight=150 (not matched by component)
    builder.add_glyph(
        "composite",
        lambda glyph: (
            glyph.add_component("base", (0, 0))
            .add_master_layer()
            .add_component("base", (0, 0))
            .add_brace_layer([150], associated_master_idx=0)
            .add_component("base", (0, 0))
        ),
    )

    propagate_all_anchors(builder.font)

    composite = builder.font.glyphs["composite"]
    brace_layers = [l for l in composite.layers if l._is_brace_layer()]
    assert len(brace_layers) == 1

    # The component has 3 sources: weight=100, 200, 300.
    # Interpolating at weight=150 with a 3-source model should use the brace
    # layer at 200 as well, giving a different result than simple 2-master lerp.
    # With 2-master linear:
    #   top = (100, 600) + 0.25 * ((300, 800) - (100, 600)) = (150, 650)
    # With 3-source model the result differs because (220, 720) at weight=200 is
    # off the linear path between (100, 600) and (300, 800).
    anchors = brace_layers[0].anchors
    assert len(anchors) == 1
    assert anchors[0].name == "top"
    # Verify it's NOT the 2-master linear result
    assert (round(anchors[0].position.x), round(anchors[0].position.y)) != (150, 650)


def test_interpolate_brace_uses_default_source_anchor_names():
    """Canonical anchor names should come from the default source location.

    If an anchor is present in the default source but missing from another source,
    it must still be interpolated (sparse interpolation), not dropped.
    """
    builder = GlyphSetBuilder()
    master0 = GSFontMaster()
    master0.id = "master-0"
    master0.name = "Bold"
    master0.axes = [200]
    builder.font.masters.append(master0)
    master1 = GSFontMaster()
    master1.id = "master-1"
    master1.name = "Regular"
    master1.axes = [100]
    builder.font.masters.append(master1)
    # Make default source != first source layer
    builder.font.customParameters["Variable Font Origin"] = "master-1"

    # Component:
    # - non-default source (master-0): top only
    # - default source (master-1): top + bottom
    builder.add_glyph(
        "base",
        lambda glyph: (
            glyph.add_anchor("top", (200, 700))
            .add_master_layer()
            .add_anchor("top", (100, 600))
            .add_anchor("bottom", (100, 0))
        ),
    )

    # Composite has a brace layer at weight=150; component has no matching brace layer
    builder.add_glyph(
        "comp",
        lambda glyph: (
            glyph.add_component("base", (0, 0))
            .add_master_layer()
            .add_component("base", (0, 0))
            .add_brace_layer([150], associated_master_idx=0)
            .add_component("base", (0, 0))
        ),
    )

    propagate_all_anchors(builder.font)

    comp = builder.font.glyphs["comp"]
    brace_layers = [l for l in comp.layers if l._is_brace_layer()]
    assert len(brace_layers) == 1
    # `bottom` must not be dropped just because it's missing in non-default source.
    assert_anchors(
        brace_layers[0].anchors,
        [("top", (150, 650)), ("bottom", (100, 0))],
    )
