# coding=utf-8

import os
from pathlib import Path
import re
import shutil

import pytest
from fontTools import ttLib
from fontTools.designspaceLib import (
    AxisDescriptor,
    AxisMappingDescriptor,
    AxisLabelDescriptor,
    DesignSpaceDocument,
    DesignSpaceDocumentError,
    DiscreteAxisDescriptor,
    InstanceDescriptor,
    RuleDescriptor,
    SourceDescriptor,
    evaluateRule,
    posix,
    processRules,
)
from fontTools.designspaceLib.types import Range
from fontTools.misc import plistlib

from .fixtures import datadir


def _axesAsDict(axes):
    """
    Make the axis data we have available in
    """
    axesDict = {}
    for axisDescriptor in axes:
        d = {
            "name": axisDescriptor.name,
            "tag": axisDescriptor.tag,
            "minimum": axisDescriptor.minimum,
            "maximum": axisDescriptor.maximum,
            "default": axisDescriptor.default,
            "map": axisDescriptor.map,
        }
        axesDict[axisDescriptor.name] = d
    return axesDict


def assert_equals_test_file(path, test_filename):
    with open(path, encoding="utf-8") as fp:
        actual = fp.read()

    test_path = os.path.join(os.path.dirname(__file__), test_filename)
    with open(test_path, encoding="utf-8") as fp:
        expected = fp.read()
        expected = re.sub(r"<!--(.|\n)*?-->", "", expected)
        expected = re.sub(r"\s*\n+", "\n", expected)

    assert actual == expected


def test_fill_document(tmpdir):
    tmpdir = str(tmpdir)
    testDocPath = os.path.join(tmpdir, "test_v4.designspace")
    testDocPath5 = os.path.join(tmpdir, "test_v5.designspace")
    masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
    masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
    instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
    instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
    doc = DesignSpaceDocument()
    doc.rulesProcessingLast = True

    # write some axes
    a1 = AxisDescriptor()
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0
    a1.name = "weight"
    a1.tag = "wght"
    # note: just to test the element language, not an actual label name recommendations.
    a1.labelNames["fa-IR"] = "قطر"
    a1.labelNames["en"] = "Wéíght"
    doc.addAxis(a1)
    a2 = AxisDescriptor()
    a2.minimum = 0
    a2.maximum = 1000
    a2.default = 15
    a2.name = "width"
    a2.tag = "wdth"
    a2.map = [(0.0, 10.0), (15.0, 20.0), (401.0, 66.0), (1000.0, 990.0)]
    a2.hidden = True
    a2.labelNames["fr"] = "Chasse"
    doc.addAxis(a2)

    # add master 1
    s1 = SourceDescriptor()
    s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
    assert s1.font is None
    s1.name = "master.ufo1"
    s1.copyLib = True
    s1.copyInfo = True
    s1.copyFeatures = True
    s1.location = dict(weight=0)
    s1.familyName = "MasterFamilyName"
    s1.styleName = "MasterStyleNameOne"
    s1.mutedGlyphNames.append("A")
    s1.mutedGlyphNames.append("Z")
    doc.addSource(s1)
    # add master 2
    s2 = SourceDescriptor()
    s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
    s2.name = "master.ufo2"
    s2.copyLib = False
    s2.copyInfo = False
    s2.copyFeatures = False
    s2.muteKerning = True
    s2.location = dict(weight=1000)
    s2.familyName = "MasterFamilyName"
    s2.styleName = "MasterStyleNameTwo"
    doc.addSource(s2)
    # add master 3 from a different layer
    s3 = SourceDescriptor()
    s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
    s3.name = "master.ufo2"
    s3.copyLib = False
    s3.copyInfo = False
    s3.copyFeatures = False
    s3.muteKerning = False
    s3.layerName = "supports"
    s3.location = dict(weight=1000)
    s3.familyName = "MasterFamilyName"
    s3.styleName = "Supports"
    doc.addSource(s3)
    # add instance 1
    i1 = InstanceDescriptor()
    i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
    i1.familyName = "InstanceFamilyName"
    i1.styleName = "InstanceStyleName"
    i1.name = "instance.ufo1"
    i1.location = dict(
        weight=500, spooky=666
    )  # this adds a dimension that is not defined.
    i1.postScriptFontName = "InstancePostscriptName"
    i1.styleMapFamilyName = "InstanceStyleMapFamilyName"
    i1.styleMapStyleName = "InstanceStyleMapStyleName"
    i1.localisedStyleName = dict(fr="Demigras", ja="半ば")
    i1.localisedFamilyName = dict(fr="Montserrat", ja="モンセラート")
    i1.localisedStyleMapStyleName = dict(de="Standard")
    i1.localisedStyleMapFamilyName = dict(
        de="Montserrat Halbfett", ja="モンセラート SemiBold"
    )
    glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125])
    i1.glyphs["arrow"] = glyphData
    i1.lib["com.coolDesignspaceApp.binaryData"] = plistlib.Data(b"<binary gunk>")
    i1.lib["com.coolDesignspaceApp.specimenText"] = "Hamburgerwhatever"
    doc.addInstance(i1)
    # add instance 2
    i2 = InstanceDescriptor()
    i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath))
    i2.familyName = "InstanceFamilyName"
    i2.styleName = "InstanceStyleName"
    i2.name = "instance.ufo2"
    # anisotropic location
    i2.location = dict(weight=500, width=(400, 300))
    i2.postScriptFontName = "InstancePostscriptName"
    i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
    i2.styleMapStyleName = "InstanceStyleMapStyleName"
    glyphMasters = [
        dict(font="master.ufo1", glyphName="BB", location=dict(width=20, weight=20)),
        dict(font="master.ufo2", glyphName="CC", location=dict(width=900, weight=900)),
    ]
    glyphData = dict(name="arrow", unicodes=[101, 201, 301])
    glyphData["masters"] = glyphMasters
    glyphData["note"] = "A note about this glyph"
    glyphData["instanceLocation"] = dict(width=100, weight=120)
    i2.glyphs["arrow"] = glyphData
    i2.glyphs["arrow2"] = dict(mute=False)
    doc.addInstance(i2)

    doc.filename = "suggestedFileName.designspace"
    doc.lib["com.coolDesignspaceApp.previewSize"] = 30

    # write some rules
    r1 = RuleDescriptor()
    r1.name = "named.rule.1"
    r1.conditionSets.append(
        [
            dict(name="axisName_a", minimum=0, maximum=1),
            dict(name="axisName_b", minimum=2, maximum=3),
        ]
    )
    r1.subs.append(("a", "a.alt"))
    doc.addRule(r1)
    # write the document; without an explicit format it will be 5.0 by default
    doc.write(testDocPath5)
    assert os.path.exists(testDocPath5)
    assert_equals_test_file(testDocPath5, "data/test_v5_original.designspace")
    # write again with an explicit format = 4.1
    doc.formatVersion = "4.1"
    doc.write(testDocPath)
    assert os.path.exists(testDocPath)
    assert_equals_test_file(testDocPath, "data/test_v4_original.designspace")
    # import it again
    new = DesignSpaceDocument()
    new.read(testDocPath)

    assert new.default.location == {"width": 20.0, "weight": 0.0}
    assert new.filename == "test_v4.designspace"
    assert new.lib == doc.lib
    assert new.instances[0].lib == doc.instances[0].lib

    # test roundtrip for the axis attributes and data
    axes = {}
    for axis in doc.axes:
        if axis.tag not in axes:
            axes[axis.tag] = []
        axes[axis.tag].append(axis.serialize())
    for axis in new.axes:
        if axis.tag[0] == "_":
            continue
        if axis.tag not in axes:
            axes[axis.tag] = []
        axes[axis.tag].append(axis.serialize())
    for v in axes.values():
        a, b = v
        assert a == b


def test_unicodes(tmpdir):
    tmpdir = str(tmpdir)
    testDocPath = os.path.join(tmpdir, "testUnicodes.designspace")
    testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace")
    masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
    masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
    instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
    instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
    doc = DesignSpaceDocument()
    doc.formatVersion = "4.1"  # This test about instance glyphs is deprecated in v5
    # add master 1
    s1 = SourceDescriptor()
    s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
    s1.name = "master.ufo1"
    s1.copyInfo = True
    s1.location = dict(weight=0)
    doc.addSource(s1)
    # add master 2
    s2 = SourceDescriptor()
    s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
    s2.name = "master.ufo2"
    s2.location = dict(weight=1000)
    doc.addSource(s2)
    # add instance 1
    i1 = InstanceDescriptor()
    i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
    i1.name = "instance.ufo1"
    i1.location = dict(weight=500)
    glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300])
    i1.glyphs["arrow"] = glyphData
    doc.addInstance(i1)
    # now we have sources and instances, but no axes yet.
    doc.axes = []  # clear the axes
    # write some axes
    a1 = AxisDescriptor()
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0
    a1.name = "weight"
    a1.tag = "wght"
    doc.addAxis(a1)
    # write the document
    doc.write(testDocPath)
    assert os.path.exists(testDocPath)
    # import it again
    new = DesignSpaceDocument()
    new.read(testDocPath)
    new.write(testDocPath2)
    # compare the file contents
    with open(testDocPath, "r", encoding="utf-8") as f1:
        t1 = f1.read()
    with open(testDocPath2, "r", encoding="utf-8") as f2:
        t2 = f2.read()
    assert t1 == t2
    # check the unicode values read from the document
    assert new.instances[0].glyphs["arrow"]["unicodes"] == [100, 200, 300]


def test_localisedNames(tmpdir):
    tmpdir = str(tmpdir)
    testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace")
    testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace")
    masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
    masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
    instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
    instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
    doc = DesignSpaceDocument()
    # add master 1
    s1 = SourceDescriptor()
    s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
    s1.name = "master.ufo1"
    s1.copyInfo = True
    s1.location = dict(weight=0)
    doc.addSource(s1)
    # add master 2
    s2 = SourceDescriptor()
    s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
    s2.name = "master.ufo2"
    s2.location = dict(weight=1000)
    doc.addSource(s2)
    # add instance 1
    i1 = InstanceDescriptor()
    i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
    i1.familyName = "Montserrat"
    i1.styleName = "SemiBold"
    i1.styleMapFamilyName = "Montserrat SemiBold"
    i1.styleMapStyleName = "Regular"
    i1.setFamilyName("Montserrat", "fr")
    i1.setFamilyName("モンセラート", "ja")
    i1.setStyleName("Demigras", "fr")
    i1.setStyleName("半ば", "ja")
    i1.setStyleMapStyleName("Standard", "de")
    i1.setStyleMapFamilyName("Montserrat Halbfett", "de")
    i1.setStyleMapFamilyName("モンセラート SemiBold", "ja")
    i1.name = "instance.ufo1"
    i1.location = dict(
        weight=500, spooky=666
    )  # this adds a dimension that is not defined.
    i1.postScriptFontName = "InstancePostscriptName"
    glyphData = dict(name="arrow", mute=True, unicodes=[0x123])
    i1.glyphs["arrow"] = glyphData
    doc.addInstance(i1)
    # now we have sources and instances, but no axes yet.
    doc.axes = []  # clear the axes
    # write some axes
    a1 = AxisDescriptor()
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0
    a1.name = "weight"
    a1.tag = "wght"
    # note: just to test the element language, not an actual label name recommendations.
    a1.labelNames["fa-IR"] = "قطر"
    a1.labelNames["en"] = "Wéíght"
    doc.addAxis(a1)
    a2 = AxisDescriptor()
    a2.minimum = 0
    a2.maximum = 1000
    a2.default = 0
    a2.name = "width"
    a2.tag = "wdth"
    a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
    a2.labelNames["fr"] = "Poids"
    doc.addAxis(a2)
    # add an axis that is not part of any location to see if that works
    a3 = AxisDescriptor()
    a3.minimum = 333
    a3.maximum = 666
    a3.default = 444
    a3.name = "spooky"
    a3.tag = "spok"
    a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
    # doc.addAxis(a3)    # uncomment this line to test the effects of default axes values
    # write some rules
    r1 = RuleDescriptor()
    r1.name = "named.rule.1"
    r1.conditionSets.append(
        [
            dict(name="weight", minimum=200, maximum=500),
            dict(name="width", minimum=0, maximum=150),
        ]
    )
    r1.subs.append(("a", "a.alt"))
    doc.addRule(r1)
    # write the document
    doc.write(testDocPath)
    assert os.path.exists(testDocPath)
    # import it again
    new = DesignSpaceDocument()
    new.read(testDocPath)
    new.write(testDocPath2)
    with open(testDocPath, "r", encoding="utf-8") as f1:
        t1 = f1.read()
    with open(testDocPath2, "r", encoding="utf-8") as f2:
        t2 = f2.read()
    assert t1 == t2


def test_handleNoAxes(tmpdir):
    tmpdir = str(tmpdir)
    # test what happens if the designspacedocument has no axes element.
    testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace")
    testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace")
    masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
    masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
    instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
    instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")

    # Case 1: No axes element in the document, but there are sources and instances
    doc = DesignSpaceDocument()

    for name, value in [("One", 1), ("Two", 2), ("Three", 3)]:
        a = AxisDescriptor()
        a.minimum = 0
        a.maximum = 1000
        a.default = 0
        a.name = "axisName%s" % (name)
        a.tag = "ax_%d" % (value)
        doc.addAxis(a)

    # add master 1
    s1 = SourceDescriptor()
    s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
    s1.name = "master.ufo1"
    s1.copyLib = True
    s1.copyInfo = True
    s1.copyFeatures = True
    s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000)
    s1.familyName = "MasterFamilyName"
    s1.styleName = "MasterStyleNameOne"
    doc.addSource(s1)

    # add master 2
    s2 = SourceDescriptor()
    s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath))
    s2.name = "master.ufo1"
    s2.copyLib = False
    s2.copyInfo = False
    s2.copyFeatures = False
    s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0)
    s2.familyName = "MasterFamilyName"
    s2.styleName = "MasterStyleNameTwo"
    doc.addSource(s2)

    # add instance 1
    i1 = InstanceDescriptor()
    i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath))
    i1.familyName = "InstanceFamilyName"
    i1.styleName = "InstanceStyleName"
    i1.name = "instance.ufo1"
    i1.location = dict(axisNameOne=(-1000, 500), axisNameTwo=100)
    i1.postScriptFontName = "InstancePostscriptName"
    i1.styleMapFamilyName = "InstanceStyleMapFamilyName"
    i1.styleMapStyleName = "InstanceStyleMapStyleName"
    doc.addInstance(i1)

    doc.write(testDocPath)
    verify = DesignSpaceDocument()
    verify.read(testDocPath)
    verify.write(testDocPath2)


def test_pathNameResolve(tmpdir):
    tmpdir = str(tmpdir)
    # test how descriptor.path and descriptor.filename are resolved
    testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace")
    testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace")
    testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace")
    testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace")
    testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace")
    testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace")
    masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
    masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
    instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
    instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")

    a1 = AxisDescriptor()
    a1.tag = "TAGA"
    a1.name = "axisName_a"
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0

    # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file.
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = None
    s.path = None
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.addSource(s)
    doc.write(testDocPath1)
    verify = DesignSpaceDocument()
    verify.read(testDocPath1)
    assert verify.sources[0].filename == None
    assert verify.sources[0].path == None

    # Case 2: filename is empty, path points somewhere: calculate a new filename.
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = None
    s.path = masterPath1
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.addSource(s)
    doc.write(testDocPath2)
    verify = DesignSpaceDocument()
    verify.read(testDocPath2)
    assert verify.sources[0].filename == "masters/masterTest1.ufo"
    assert verify.sources[0].path == posix(masterPath1)

    # Case 3: the filename is set, the path is None.
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = "../somewhere/over/the/rainbow.ufo"
    s.path = None
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.addSource(s)
    doc.write(testDocPath3)
    verify = DesignSpaceDocument()
    verify.read(testDocPath3)
    assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
    # make the absolute path for filename so we can see if it matches the path
    p = os.path.abspath(
        os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename)
    )
    assert verify.sources[0].path == posix(p)

    # Case 4: the filename points to one file, the path points to another. The path takes precedence.
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = "../somewhere/over/the/rainbow.ufo"
    s.path = masterPath1
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.addSource(s)
    doc.write(testDocPath4)
    verify = DesignSpaceDocument()
    verify.read(testDocPath4)
    assert verify.sources[0].filename == "masters/masterTest1.ufo"

    # Case 5: the filename is None, path has a value, update the filename
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = None
    s.path = masterPath1
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.addSource(s)
    doc.write(testDocPath5)  # so that the document has a path
    doc.updateFilenameFromPath()
    assert doc.sources[0].filename == "masters/masterTest1.ufo"

    # Case 6: the filename has a value, path has a value, update the filenames with force
    doc = DesignSpaceDocument()
    doc.addAxis(a1)
    s = SourceDescriptor()
    s.filename = "../somewhere/over/the/rainbow.ufo"
    s.path = masterPath1
    s.copyInfo = True
    s.location = dict(weight=0)
    s.familyName = "MasterFamilyName"
    s.styleName = "MasterStyleNameOne"
    doc.write(testDocPath5)  # so that the document has a path
    doc.addSource(s)
    assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo"
    doc.updateFilenameFromPath(force=True)
    assert doc.sources[0].filename == "masters/masterTest1.ufo"


def test_normalise1():
    # normalisation of anisotropic locations, clipping
    doc = DesignSpaceDocument()
    # write some axes
    a1 = AxisDescriptor()
    a1.minimum = -1000
    a1.maximum = 1000
    a1.default = 0
    a1.name = "axisName_a"
    a1.tag = "TAGA"
    doc.addAxis(a1)
    assert doc.normalizeLocation(dict(axisName_a=0)) == {"axisName_a": 0.0}
    assert doc.normalizeLocation(dict(axisName_a=1000)) == {"axisName_a": 1.0}
    # clipping beyond max values:
    assert doc.normalizeLocation(dict(axisName_a=1001)) == {"axisName_a": 1.0}
    assert doc.normalizeLocation(dict(axisName_a=500)) == {"axisName_a": 0.5}
    assert doc.normalizeLocation(dict(axisName_a=-1000)) == {"axisName_a": -1.0}
    assert doc.normalizeLocation(dict(axisName_a=-1001)) == {"axisName_a": -1.0}
    # anisotropic coordinates normalise to isotropic
    assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {"axisName_a": 1.0}
    doc.normalize()
    r = []
    for axis in doc.axes:
        r.append((axis.name, axis.minimum, axis.default, axis.maximum))
    r.sort()
    assert r == [("axisName_a", -1.0, 0.0, 1.0)]


def test_normalise2():
    # normalisation with minimum > 0
    doc = DesignSpaceDocument()
    # write some axes
    a2 = AxisDescriptor()
    a2.minimum = 100
    a2.maximum = 1000
    a2.default = 100
    a2.name = "axisName_b"
    doc.addAxis(a2)
    assert doc.normalizeLocation(dict(axisName_b=0)) == {"axisName_b": 0.0}
    assert doc.normalizeLocation(dict(axisName_b=1000)) == {"axisName_b": 1.0}
    # clipping beyond max values:
    assert doc.normalizeLocation(dict(axisName_b=1001)) == {"axisName_b": 1.0}
    assert doc.normalizeLocation(dict(axisName_b=500)) == {
        "axisName_b": 0.4444444444444444
    }
    assert doc.normalizeLocation(dict(axisName_b=-1000)) == {"axisName_b": 0.0}
    assert doc.normalizeLocation(dict(axisName_b=-1001)) == {"axisName_b": 0.0}
    # anisotropic coordinates normalise to isotropic
    assert doc.normalizeLocation(dict(axisName_b=(1000, -1000))) == {"axisName_b": 1.0}
    assert doc.normalizeLocation(dict(axisName_b=1001)) == {"axisName_b": 1.0}
    doc.normalize()
    r = []
    for axis in doc.axes:
        r.append((axis.name, axis.minimum, axis.default, axis.maximum))
    r.sort()
    assert r == [("axisName_b", 0.0, 0.0, 1.0)]


def test_normalise3():
    # normalisation of negative values, with default == maximum
    doc = DesignSpaceDocument()
    # write some axes
    a3 = AxisDescriptor()
    a3.minimum = -1000
    a3.maximum = 0
    a3.default = 0
    a3.name = "ccc"
    doc.addAxis(a3)
    assert doc.normalizeLocation(dict(ccc=0)) == {"ccc": 0.0}
    assert doc.normalizeLocation(dict(ccc=1)) == {"ccc": 0.0}
    assert doc.normalizeLocation(dict(ccc=-1000)) == {"ccc": -1.0}
    assert doc.normalizeLocation(dict(ccc=-1001)) == {"ccc": -1.0}
    doc.normalize()
    r = []
    for axis in doc.axes:
        r.append((axis.name, axis.minimum, axis.default, axis.maximum))
    r.sort()
    assert r == [("ccc", -1.0, 0.0, 0.0)]


def test_normalise4():
    # normalisation with a map
    doc = DesignSpaceDocument()
    # write some axes
    a4 = AxisDescriptor()
    a4.minimum = 0
    a4.maximum = 1000
    a4.default = 0
    a4.name = "ddd"
    a4.map = [(0, 100), (300, 500), (600, 500), (1000, 900)]
    doc.addAxis(a4)
    doc.normalize()
    r = []
    for axis in doc.axes:
        r.append((axis.name, axis.map))
    r.sort()
    assert r == [("ddd", [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])]


def test_axisMapping():
    # note: because designspance lib does not do any actual
    # processing of the mapping data, we can only check if there data is there.
    doc = DesignSpaceDocument()
    # write some axes
    a4 = AxisDescriptor()
    a4.minimum = 0
    a4.maximum = 1000
    a4.default = 0
    a4.name = "ddd"
    a4.map = [(0, 100), (300, 500), (600, 500), (1000, 900)]
    doc.addAxis(a4)
    doc.normalize()
    r = []
    for axis in doc.axes:
        r.append((axis.name, axis.map))
    r.sort()
    assert r == [("ddd", [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])]


def test_axisMappingsRoundtrip(tmpdir):
    # tests of axisMappings in a document, roundtripping.

    tmpdir = str(tmpdir)
    srcDocPath = (Path(__file__) / "../data/test_avar2.designspace").resolve()
    testDocPath = os.path.join(tmpdir, "test_avar2.designspace")
    shutil.copy(srcDocPath, testDocPath)
    testDocPath2 = os.path.join(tmpdir, "test_avar2_roundtrip.designspace")
    doc = DesignSpaceDocument()
    doc.read(testDocPath)
    assert doc.axisMappings
    assert len(doc.axisMappings) == 2
    assert doc.axisMappings[0].inputLocation == {"Justify": -100.0, "Width": 100.0}

    # This is a bit of a hack, but it's the only way to make sure
    # that the save works on Windows if the tempdir and the data
    # dir are on different drives.
    for descriptor in doc.sources + doc.instances:
        descriptor.path = None

    doc.write(testDocPath2)
    # verify these results
    doc2 = DesignSpaceDocument()
    doc2.read(testDocPath2)
    assert [mapping.inputLocation for mapping in doc.axisMappings] == [
        mapping.inputLocation for mapping in doc2.axisMappings
    ]
    assert [mapping.outputLocation for mapping in doc.axisMappings] == [
        mapping.outputLocation for mapping in doc2.axisMappings
    ]
    assert [mapping.description for mapping in doc.axisMappings] == [
        mapping.description for mapping in doc2.axisMappings
    ]
    assert [mapping.groupDescription for mapping in doc.axisMappings] == [
        mapping.groupDescription for mapping in doc2.axisMappings
    ]


def test_rulesConditions(tmpdir):
    # tests of rules, conditionsets and conditions
    r1 = RuleDescriptor()
    r1.name = "named.rule.1"
    r1.conditionSets.append(
        [
            dict(name="axisName_a", minimum=0, maximum=1000),
            dict(name="axisName_b", minimum=0, maximum=3000),
        ]
    )
    r1.subs.append(("a", "a.alt"))

    assert evaluateRule(r1, dict(axisName_a=500, axisName_b=0)) == True
    assert evaluateRule(r1, dict(axisName_a=0, axisName_b=0)) == True
    assert evaluateRule(r1, dict(axisName_a=1000, axisName_b=0)) == True
    assert evaluateRule(r1, dict(axisName_a=1000, axisName_b=-100)) == False
    assert evaluateRule(r1, dict(axisName_a=1000.0001, axisName_b=0)) == False
    assert evaluateRule(r1, dict(axisName_a=-0.0001, axisName_b=0)) == False
    assert evaluateRule(r1, dict(axisName_a=-100, axisName_b=0)) == False
    assert processRules([r1], dict(axisName_a=500, axisName_b=0), ["a", "b", "c"]) == [
        "a.alt",
        "b",
        "c",
    ]
    assert processRules(
        [r1], dict(axisName_a=500, axisName_b=0), ["a.alt", "b", "c"]
    ) == ["a.alt", "b", "c"]
    assert processRules([r1], dict(axisName_a=2000, axisName_b=0), ["a", "b", "c"]) == [
        "a",
        "b",
        "c",
    ]

    # rule with only a maximum
    r2 = RuleDescriptor()
    r2.name = "named.rule.2"
    r2.conditionSets.append([dict(name="axisName_a", maximum=500)])
    r2.subs.append(("b", "b.alt"))

    assert evaluateRule(r2, dict(axisName_a=0)) == True
    assert evaluateRule(r2, dict(axisName_a=-500)) == True
    assert evaluateRule(r2, dict(axisName_a=1000)) == False

    # rule with only a minimum
    r3 = RuleDescriptor()
    r3.name = "named.rule.3"
    r3.conditionSets.append([dict(name="axisName_a", minimum=500)])
    r3.subs.append(("c", "c.alt"))

    assert evaluateRule(r3, dict(axisName_a=0)) == False
    assert evaluateRule(r3, dict(axisName_a=1000)) == True
    assert evaluateRule(r3, dict(axisName_a=1000)) == True

    # rule with only a minimum, maximum in separate conditions
    r4 = RuleDescriptor()
    r4.name = "named.rule.4"
    r4.conditionSets.append(
        [dict(name="axisName_a", minimum=500), dict(name="axisName_b", maximum=500)]
    )
    r4.subs.append(("c", "c.alt"))

    assert evaluateRule(r4, dict(axisName_a=1000, axisName_b=0)) == True
    assert evaluateRule(r4, dict(axisName_a=0, axisName_b=0)) == False
    assert evaluateRule(r4, dict(axisName_a=1000, axisName_b=1000)) == False


def test_rulesDocument(tmpdir):
    # tests of rules in a document, roundtripping.
    tmpdir = str(tmpdir)
    testDocPath = os.path.join(tmpdir, "testRules.designspace")
    testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace")
    doc = DesignSpaceDocument()
    doc.rulesProcessingLast = True
    a1 = AxisDescriptor()
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0
    a1.name = "axisName_a"
    a1.tag = "TAGA"
    b1 = AxisDescriptor()
    b1.minimum = 2000
    b1.maximum = 3000
    b1.default = 2000
    b1.name = "axisName_b"
    b1.tag = "TAGB"
    doc.addAxis(a1)
    doc.addAxis(b1)
    r1 = RuleDescriptor()
    r1.name = "named.rule.1"
    r1.conditionSets.append(
        [
            dict(name="axisName_a", minimum=0, maximum=1000),
            dict(name="axisName_b", minimum=0, maximum=3000),
        ]
    )
    r1.subs.append(("a", "a.alt"))
    # rule with minium and maximum
    doc.addRule(r1)
    assert len(doc.rules) == 1
    assert len(doc.rules[0].conditionSets) == 1
    assert len(doc.rules[0].conditionSets[0]) == 2
    assert _axesAsDict(doc.axes) == {
        "axisName_a": {
            "map": [],
            "name": "axisName_a",
            "default": 0,
            "minimum": 0,
            "maximum": 1000,
            "tag": "TAGA",
        },
        "axisName_b": {
            "map": [],
            "name": "axisName_b",
            "default": 2000,
            "minimum": 2000,
            "maximum": 3000,
            "tag": "TAGB",
        },
    }
    assert doc.rules[0].conditionSets == [
        [
            {"minimum": 0, "maximum": 1000, "name": "axisName_a"},
            {"minimum": 0, "maximum": 3000, "name": "axisName_b"},
        ]
    ]
    assert doc.rules[0].subs == [("a", "a.alt")]
    doc.normalize()
    assert doc.rules[0].name == "named.rule.1"
    assert doc.rules[0].conditionSets == [
        [
            {"minimum": 0.0, "maximum": 1.0, "name": "axisName_a"},
            {"minimum": 0.0, "maximum": 1.0, "name": "axisName_b"},
        ]
    ]
    # still one conditionset
    assert len(doc.rules[0].conditionSets) == 1
    doc.write(testDocPath)
    # add a stray conditionset
    _addUnwrappedCondition(testDocPath)
    doc2 = DesignSpaceDocument()
    doc2.read(testDocPath)
    assert doc2.rulesProcessingLast
    assert len(doc2.axes) == 2
    assert len(doc2.rules) == 1
    assert len(doc2.rules[0].conditionSets) == 2
    doc2.write(testDocPath2)
    # verify these results
    # make sure the stray condition is now neatly wrapped in a conditionset.
    doc3 = DesignSpaceDocument()
    doc3.read(testDocPath2)
    assert len(doc3.rules) == 1
    assert len(doc3.rules[0].conditionSets) == 2


def _addUnwrappedCondition(path):
    # only for testing, so we can make an invalid designspace file
    # older designspace files may have conditions that are not wrapped in a conditionset
    # These can be read into a new conditionset.
    with open(path, "r", encoding="utf-8") as f:
        d = f.read()
    print(d)
    d = d.replace(
        '<rule name="named.rule.1">',
        '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />',
    )
    with open(path, "w", encoding="utf-8") as f:
        f.write(d)


def test_documentLib(tmpdir):
    # roundtrip test of the document lib with some nested data
    tmpdir = str(tmpdir)
    testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace")
    doc = DesignSpaceDocument()
    a1 = AxisDescriptor()
    a1.tag = "TAGA"
    a1.name = "axisName_a"
    a1.minimum = 0
    a1.maximum = 1000
    a1.default = 0
    doc.addAxis(a1)
    dummyData = dict(a=123, b="äbc", c=[1, 2, 3], d={"a": 123})
    dummyKey = "org.fontTools.designspaceLib"
    doc.lib = {dummyKey: dummyData}
    doc.write(testDocPath1)
    new = DesignSpaceDocument()
    new.read(testDocPath1)
    assert dummyKey in new.lib
    assert new.lib[dummyKey] == dummyData


def test_updatePaths(tmpdir):
    doc = DesignSpaceDocument()
    doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace")

    s1 = SourceDescriptor()
    doc.addSource(s1)

    doc.updatePaths()

    # expect no changes
    assert s1.path is None
    assert s1.filename is None

    name1 = "../masters/Source1.ufo"
    path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo"))

    s1.path = path1
    s1.filename = None

    doc.updatePaths()

    assert s1.path == path1
    assert s1.filename == name1  # empty filename updated

    name2 = "../masters/Source2.ufo"
    s1.filename = name2

    doc.updatePaths()

    # conflicting filename discarded, path always gets precedence
    assert s1.path == path1
    assert s1.filename == "../masters/Source1.ufo"

    s1.path = None
    s1.filename = name2

    doc.updatePaths()

    # expect no changes
    assert s1.path is None
    assert s1.filename == name2


def test_read_with_path_object():
    source = (Path(__file__) / "../data/test_v4_original.designspace").resolve()
    assert source.exists()
    doc = DesignSpaceDocument()
    doc.read(source)


def test_with_with_path_object(tmpdir):
    tmpdir = str(tmpdir)
    dest = Path(tmpdir) / "test_v4_original.designspace"
    doc = DesignSpaceDocument()
    doc.write(dest)
    assert dest.exists()


def test_findDefault_axis_mapping():
    designspace_string = """\
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="4.0">
  <axes>
    <axis tag="wght" name="Weight" minimum="100" maximum="800" default="400">
      <map input="100" output="20"/>
      <map input="300" output="40"/>
      <map input="400" output="80"/>
      <map input="700" output="126"/>
      <map input="800" output="170"/>
    </axis>
    <axis tag="ital" name="Italic" minimum="0" maximum="1" default="1"/>
  </axes>
  <sources>
    <source filename="Font-Light.ufo">
      <location>
        <dimension name="Weight" xvalue="20"/>
        <dimension name="Italic" xvalue="0"/>
      </location>
    </source>
    <source filename="Font-Regular.ufo">
      <location>
        <dimension name="Weight" xvalue="80"/>
        <dimension name="Italic" xvalue="0"/>
      </location>
    </source>
    <source filename="Font-Bold.ufo">
      <location>
        <dimension name="Weight" xvalue="170"/>
        <dimension name="Italic" xvalue="0"/>
      </location>
    </source>
    <source filename="Font-LightItalic.ufo">
      <location>
        <dimension name="Weight" xvalue="20"/>
        <dimension name="Italic" xvalue="1"/>
      </location>
    </source>
    <source filename="Font-Italic.ufo">
      <location>
        <dimension name="Weight" xvalue="80"/>
        <dimension name="Italic" xvalue="1"/>
      </location>
    </source>
    <source filename="Font-BoldItalic.ufo">
      <location>
        <dimension name="Weight" xvalue="170"/>
        <dimension name="Italic" xvalue="1"/>
      </location>
    </source>
  </sources>
</designspace>
    """
    designspace = DesignSpaceDocument.fromstring(designspace_string)
    assert designspace.findDefault().filename == "Font-Italic.ufo"

    designspace.axes[1].default = 0

    assert designspace.findDefault().filename == "Font-Regular.ufo"


def test_loadSourceFonts():
    def opener(path):
        font = ttLib.TTFont()
        font.importXML(path)
        return font

    # this designspace file contains .TTX source paths
    path = os.path.join(
        os.path.dirname(os.path.dirname(__file__)),
        "varLib",
        "data",
        "SparseMasters.designspace",
    )
    designspace = DesignSpaceDocument.fromfile(path)

    # force two source descriptors to have the same path
    designspace.sources[1].path = designspace.sources[0].path

    fonts = designspace.loadSourceFonts(opener)

    assert len(fonts) == 3
    assert all(isinstance(font, ttLib.TTFont) for font in fonts)
    assert fonts[0] is fonts[1]  # same path, identical font object

    fonts2 = designspace.loadSourceFonts(opener)

    for font1, font2 in zip(fonts, fonts2):
        assert font1 is font2


def test_loadSourceFonts_no_required_path():
    designspace = DesignSpaceDocument()
    designspace.sources.append(SourceDescriptor())

    with pytest.raises(DesignSpaceDocumentError, match="no 'path' attribute"):
        designspace.loadSourceFonts(lambda p: p)


def test_addAxisDescriptor():
    ds = DesignSpaceDocument()

    axis = ds.addAxisDescriptor(
        name="Weight", tag="wght", minimum=100, default=400, maximum=900
    )

    assert ds.axes[0] is axis
    assert isinstance(axis, AxisDescriptor)
    assert axis.name == "Weight"
    assert axis.tag == "wght"
    assert axis.minimum == 100
    assert axis.default == 400
    assert axis.maximum == 900


def test_addAxisDescriptor():
    ds = DesignSpaceDocument()

    mapping = ds.addAxisMappingDescriptor(
        inputLocation={"weight": 900, "width": 150}, outputLocation={"weight": 870}
    )

    assert ds.axisMappings[0] is mapping
    assert isinstance(mapping, AxisMappingDescriptor)
    assert mapping.inputLocation == {"weight": 900, "width": 150}
    assert mapping.outputLocation == {"weight": 870}


def test_addSourceDescriptor():
    ds = DesignSpaceDocument()

    source = ds.addSourceDescriptor(name="TestSource", location={"Weight": 400})

    assert ds.sources[0] is source
    assert isinstance(source, SourceDescriptor)
    assert source.name == "TestSource"
    assert source.location == {"Weight": 400}


def test_addInstanceDescriptor():
    ds = DesignSpaceDocument()

    instance = ds.addInstanceDescriptor(
        name="TestInstance",
        location={"Weight": 400},
        styleName="Regular",
        styleMapStyleName="regular",
    )

    assert ds.instances[0] is instance
    assert isinstance(instance, InstanceDescriptor)
    assert instance.name == "TestInstance"
    assert instance.location == {"Weight": 400}
    assert instance.styleName == "Regular"
    assert instance.styleMapStyleName == "regular"


def test_addRuleDescriptor(tmp_path):
    ds = DesignSpaceDocument()

    rule = ds.addRuleDescriptor(
        name="TestRule",
        conditionSets=[
            [
                dict(name="Weight", minimum=100, maximum=200),
                dict(name="Weight", minimum=700, maximum=900),
            ]
        ],
        subs=[("a", "a.alt")],
    )

    assert ds.rules[0] is rule
    assert isinstance(rule, RuleDescriptor)
    assert rule.name == "TestRule"
    assert rule.conditionSets == [
        [
            dict(name="Weight", minimum=100, maximum=200),
            dict(name="Weight", minimum=700, maximum=900),
        ]
    ]
    assert rule.subs == [("a", "a.alt")]

    # Test it doesn't crash.
    ds.write(tmp_path / "test.designspace")


def test_deepcopyExceptFonts():
    ds = DesignSpaceDocument()
    ds.addSourceDescriptor(font=object())
    ds.addSourceDescriptor(font=object())

    ds_copy = ds.deepcopyExceptFonts()

    assert ds.tostring() == ds_copy.tostring()
    assert ds.sources[0].font is ds_copy.sources[0].font
    assert ds.sources[1].font is ds_copy.sources[1].font


def test_Range_post_init():
    # test min and max are sorted and default is clamped to either min/max
    r = Range(minimum=2, maximum=-1, default=-2)
    assert r.minimum == -1
    assert r.maximum == 2
    assert r.default == -1


def test_get_axes(datadir: Path) -> None:
    ds = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")

    assert ds.getAxis("Width") is ds.getAxisByTag("wdth")
    assert ds.getAxis("Italic") is ds.getAxisByTag("ital")
