"""
    The 'unit' must be ascii only.
"""
from collections import OrderedDict

import pytest
from pytest import approx

from barril import units
from barril.units import ChangeScalars
from barril.units import InvalidOperationError
from barril.units import InvalidUnitError
from barril.units import ObtainQuantity
from barril.units import Quantity
from barril.units import Scalar


def testScalarInterface(unit_database_well_length) -> None:
    s = Scalar("well-length")
    assert 0 == s.value
    assert "m" == s.unit


def testQuantity(unit_database_well_length) -> None:
    Quantity("well-length", "m")
    with pytest.raises(units.InvalidQuantityTypeError):
        Quantity("foo", "m")


def testQuantityEq(unit_database_well_length) -> None:
    q0 = ObtainQuantity("m", "well-length")
    q1 = ObtainQuantity("m", "well-length")
    assert q0 == q1


def testReadOnlyQuantityCopy(unit_database_well_length) -> None:
    q0 = ObtainQuantity("m", "well-length")
    q1 = q0.Copy()
    assert q0 == q1


def testScalar(unit_database_well_length) -> None:
    q = ObtainQuantity("m", "well-length")
    s = Scalar.CreateWithQuantity(q, 10)

    assert 10 == s.value
    assert approx(abs(393.700787402 - s.GetValue(unit="in")), 7) == 0

    s = s.CreateCopy(value=0.254, unit="in")
    assert 0.254 == s.value
    assert "in" == s.unit
    assert "well-length" == s.category

    with pytest.raises(AttributeError):
        setattr(s, "value", 10)


def GetQuantity(unit="cm"):
    return units.ObtainQuantity(unit, "well-length")


def testBadUnit(unit_database_well_length) -> None:
    x = units.Scalar.CreateWithQuantity(GetQuantity(), 500)
    with pytest.raises(units.InvalidUnitError):
        x.CreateCopy(unit="invalidUnit")


def testOperations(unit_database_well_length) -> None:
    x = units.Scalar.CreateWithQuantity(GetQuantity(), 500)
    assert x.value == 500
    assert x.unit == "cm"

    assert x.GetValue("m") == 5
    assert x.unit == "cm"
    assert x.GetValue("cm") == 500
    assert x.unit == "cm"
    assert x.GetValue("km") == 0.005


def unit(value, unit="m"):
    return units.Scalar.CreateWithQuantity(GetQuantity(unit), value)


def testEq(unit_database_well_length) -> None:
    u1 = units.Scalar.CreateWithQuantity(GetQuantity(), 1.0)
    u2 = units.Scalar.CreateWithQuantity(GetQuantity(), 1.0)
    assert u1 == u2
    assert u1.unit == u2.unit
    assert "cm" == u2.unit


def testAddUnit(unit_database_empty) -> None:
    unit_database = unit_database_empty
    unit_database.AddUnitBase("length", "metre", "m")
    RATIO = 0.0254
    unit_database.AddUnit(
        "length", "inch", "in", lambda x: 0.0254 * RATIO, lambda y: y / RATIO, default_category=None
    )
    unit_database.AddCategory("length", "length", override=True, valid_units=["m", "in"])
    unit_database.AddCategory("depth", "length", override=True, valid_units=["m", "in"])

    a = units.Scalar("length", value=1.0, unit="m")
    b = units.Scalar("length", value=2.0, unit="m")
    c = units.Scalar("length", value=1.0, unit="in")
    d = units.Scalar("depth", value=1.0, unit="m")
    e = None
    f = units.Scalar("length", value=1.0, unit="m")

    assert a != b
    assert a != c
    assert a != e
    assert a != d

    assert a == f


def testValidUnits(unit_database_empty) -> None:
    # let'scalar clear up the unit manager
    unit_database = unit_database_empty

    unit_database.AddUnitBase("length", "meters", "m")
    unit_database.AddUnit("length", "centimeters", "cm", "%f * 100.0", "%f / 100.0")
    unit_database.AddUnit("length", "milimeters", "mm", "%f * 1000.0", "%f / 1000.0")
    unit_database.AddUnit("length", "kilometers", "km", "%f / 1000.0", "%f * 1000.0")
    unit_database.AddUnit("length", "miles", "mi", "%f / 1609.347", "%f * 1609.347")
    unit_database.AddUnit("length", "inches", "in", "%f / 0.0254", "%f * 0.0254")

    valid_units = ["m", "cm", "mm"]
    unit_database.AddCategory("length", "length")
    unit_database.AddCategory("well-length", "length", valid_units=valid_units)
    unit_database.AddCategory("well-diameter", "length")

    Quantity("well-length", "km")

    scalar = units.Scalar("well-length", 1, "m")
    assert valid_units == scalar.GetValidUnits()

    scalar = units.Scalar("well-diameter", 1, "m")
    assert ["m", "cm", "mm", "km", "mi", "in"] == scalar.GetValidUnits()

    # Creating a scalar in a unit that isn't valid shouldn't raise an error and the unit
    # will be added to valid units
    scalar = units.Scalar("well-length", 1, "mi")
    assert scalar.GetValidUnits() == ["m", "cm", "mm", "mi"]


def testScalarCopyAndRepresentation(unit_database_empty) -> None:
    unit_database = unit_database_empty

    unit_database.AddUnitBase("length", "meters", "m")
    unit_database.AddUnit("length", "milimeters", "mm", "x * 1000.0", "x / 1000.0")
    unit_database.AddUnit("length", "centimeters", "cm", "x * 100.0", "x / 100.0")
    unit_database.AddCategory("well-diameter", "length")
    s = Scalar("well-diameter", 10, "m")

    assert "Scalar(10.0, 'm', 'well-diameter')" == repr(s)
    assert "10.0" == str(s.value)
    assert "m" == s.unit
    assert "well-diameter" == s.GetCategory()
    assert "length" == s.GetQuantityType()

    s = s.CreateCopy(unit="cm")
    assert "Scalar(1000.0, 'cm', 'well-diameter')" == repr(s)
    assert "1000 [cm]" == str(s)


def testSort(unit_database_empty) -> None:
    db = unit_database_empty

    db.AddUnitBase("length", "meters", "m")
    db.AddUnit("length", "centimeters", "cm", "x * 100.0", "x / 100.0")
    db.AddCategory("length", "length")

    s1 = Scalar("length", 10.0, "cm")
    s2 = Scalar("length", 1.0, "m")
    s3 = Scalar("length", 500.0, "cm")
    s4 = Scalar("length", 10.0, "m")

    x = [s2, s1, s4, s3]
    x.sort()
    assert x == [s1, s2, s3, s4]


def testCategoryParameters(unit_database_empty) -> None:
    db = unit_database_empty

    db.AddUnitBase("length", "meters", "m")
    db.AddUnit("length", "centimeters", "cm", "x * 100.0", "x / 100.0")

    db.AddCategory(
        "my length",
        "length",
        ["cm", "m"],
        default_unit="m",
        default_value=18.5,
        min_value=-30.0,
        max_value=100.0,
        is_min_exclusive=True,
        is_max_exclusive=False,
    )

    s1 = Scalar("my length")
    assert s1.value == 18.5
    assert s1.unit == "m"

    # test maximum boundary
    s2 = Scalar("my length", 100.0, "m")
    assert s2.value == 100.0
    assert s2.unit == "m"

    s3 = Scalar("my length", 500.0, "cm")
    assert s3.value == 500.0
    assert s3.unit == "cm"

    # higher than maximum value
    another = Scalar("my length", 120.0, "m")
    assert not another.IsValid()
    with pytest.raises(ValueError):
        another.CheckValidity()

    another = Scalar("my length", -30.0, "m")
    assert not another.IsValid()

    s4 = Scalar("my length", unit="cm")
    assert s4.value == 1850.0
    assert s4.unit == "cm"

    another = s4.CreateCopy(12500.00, "cm")
    assert not another.IsValid()


def testInvalidUnit(unit_database_empty) -> None:
    unit_database = unit_database_empty

    unit_database.AddUnitBase("length", "meters", "m")
    unit_database.AddUnit("length", "milimeters", "mm", "x * 1000.0", "x / 1000.0")
    unit_database.AddUnit("length", "centimeters", "cm", "x * 100.0", "x / 100.0")
    unit_database.AddCategory("well-diameter", "length")
    with pytest.raises(InvalidUnitError):
        Scalar("well-diameter", 10, "days")


def testDerivedUnits(unit_database_empty) -> None:
    unit_database = unit_database_empty

    unit_database.AddUnitBase("length", "meters", "m")
    unit_database.AddUnit("length", "milimeters", "mm", "x * 1000.0", "x / 1000.0")
    unit_database.AddUnit("length", "centimeters", "cm", "x * 100.0", "x / 100.0")
    unit_database.AddCategory("well-diameter", "length")
    unit_database.AddUnitBase("time", "seconds", "s")
    unit_database.AddCategory(category="Time", quantity_type="time")

    s1 = Scalar("well-diameter", 10, "m")
    s2 = Scalar("well-diameter", 10, "cm")
    s3 = Scalar("Time", 10, "s")

    assert Scalar("well-diameter", 10.10, "m") == s1 + s2
    assert Scalar("well-diameter", 9.90, "m") == s1 - s2

    quantity = Quantity.CreateDerived(OrderedDict())
    assert Scalar.CreateWithQuantity(quantity, 100) == s1 / s2
    assert Scalar("well-diameter", 9.90, "m") == s1 - s2
    with pytest.raises(InvalidOperationError):
        s1.__sub__(s3)


def testCreationWithDerivedQuantity(unit_database_len_time) -> None:
    unit_database = unit_database_len_time

    m = Quantity.CreateDerived(OrderedDict([("Table size", ["m", 1])]))
    km_city = Quantity.CreateDerived(OrderedDict([("City size", ["km", 1])]))

    quantity, value = unit_database.Multiply(m, km_city, 1, 0.01)
    calculated1 = Scalar.CreateWithQuantity(quantity, value)
    assert str(calculated1)

    s1 = Scalar.CreateWithQuantity(m, 1)
    s2 = Scalar.CreateWithQuantity(km_city, 0.01)
    assert calculated1 == s1 * s2


def testNumberInteractions(unit_database_len_time) -> None:
    scalar = Scalar("Table size", 1, "m")
    scalar2 = Scalar.CreateWithQuantity(Quantity.CreateDerived(OrderedDict()), 0)

    assert scalar == scalar + scalar2

    assert scalar == scalar + 0
    assert scalar == 0 + scalar

    assert 9 == (10 - scalar).value
    assert -9 == (scalar - 10).value

    assert 10 == (10 * scalar).value
    assert 10 == (scalar * 10).value

    assert 10 == (10.0 / scalar).value
    assert 1 / 10.0 == (scalar / 10.0).value


def testDivision(unit_database_len_time) -> None:
    unit_database = unit_database_len_time

    m = Quantity.CreateDerived(OrderedDict([("Table size", ["m", 1])]))
    km_city = Quantity.CreateDerived(OrderedDict([("City size", ["km", 1])]))
    quantity, value = unit_database.Divide(m, km_city, 1, 0.01)
    calculated1 = Scalar.CreateWithQuantity(quantity, value)
    s1 = Scalar.CreateWithQuantity(m, 1)
    s2 = Scalar.CreateWithQuantity(km_city, 0.01)
    assert calculated1 == s1 / s2


def testFloorDivision() -> None:
    a = Scalar(3.5, "m")
    b = Scalar(100.0, "cm")
    assert (a // b).GetValueAndUnit() == (3.0, "")
    assert (350 // b).GetValueAndUnit() == (3.0, "1/cm")
    assert (a // 1.0).GetValueAndUnit() == (3.0, "m")


def testNumberOverScalar() -> None:
    a = Scalar(2.0, "m")
    b = Scalar(3.0, "m")
    assert (1.0 / a).GetValueAndUnit() == (0.5, "1/m")
    assert (3.0 / a).GetValueAndUnit() == (1.5, "1/m")
    assert b / a == b * 1 / a == b * (1.0 / a)


def testPow(unit_database_len_time) -> None:
    s = Scalar(2, "s", "Time")
    spow = s**3
    assert spow.unit == "s3"
    assert spow.value == 8


def testFormatedUnitsOnScalar(unit_database_empty) -> None:
    """
    Allow present units on get formatted Scalar
    """
    unit_database = unit_database_empty
    unit_database.AddUnitBase("length", "meters", "m")
    unit_database.AddCategory(category="length", quantity_type="length")

    class MyScalar(units.Scalar):
        pass

    scalar = MyScalar("length", 1.18, "m")
    assert scalar.GetFormatted() == "1.18 [m]"

    MyScalar.SetFormattedValueFormat("%0.4f")
    MyScalar.SetFormattedSuffixFormat(" (%s)")

    # Tests the GET methods
    assert MyScalar.GetFormattedValueFormat() == "%0.4f"
    assert MyScalar.GetFormattedSuffixFormat() == " (%s)"

    # Tests the Formatted method
    assert scalar.GetFormatted() == "1.1800 (m)"

    # Check for invalid format strings
    with pytest.raises(TypeError):
        MyScalar.SetFormattedValueFormat("%d [%s]")
    with pytest.raises(TypeError):
        MyScalar.SetFormattedSuffixFormat("%s %s")
    with pytest.raises(TypeError):
        MyScalar.SetFormattedSuffixFormat("%f")

    # When invalid format are set, the formats are not changed
    assert MyScalar.GetFormattedValueFormat() == "%0.4f"
    assert MyScalar.GetFormattedSuffixFormat() == " (%s)"

    # Make sure we didn't affect the Scalar class, only the local MyScalar
    MyScalar.SetFormattedValueFormat("<<< %f")
    MyScalar.SetFormattedSuffixFormat(" [%s] >>>")
    scalar2 = Scalar("length", 1.18, "m")
    assert scalar2.GetFormatted() == "1.18 [m]"


def testEmptyScalar() -> None:
    """
    ScalarMultiData exception when some of its scalars don't have a quantity_type
    """
    # An empty scalar doesn't have a category defined
    scalar_1 = Scalar.CreateEmptyScalar(20.0)

    # When try to retrieve scalar value or unit a  exception was being raised
    assert scalar_1.GetValue("m") == 20.0
    assert scalar_1.GetUnit() == ""


def testCopyProperties(unit_database_well_length) -> None:
    """
    Test if the mehod SetValue is not called when copying the Scalar's properties.
    """
    category = "well-length"
    unit = "m"
    value = 10.0

    scalar_source = Scalar(category, value, unit)
    scalar_dest = scalar_source.CreateCopyInstance()

    assert scalar_dest.GetCategory() == category
    assert scalar_dest.GetUnit() == unit
    assert scalar_dest.GetValue() == value


def testCopyPropertiesAndValidation(unit_database_well_length) -> None:
    category = "well-length-with-min-and-max"  # the minimum value is zero
    unit = "m"
    value = -1

    # Not raises exception because validation is False on the copy operation
    scalar_source = Scalar(category, value, unit)
    assert not scalar_source.IsValid()

    # Raises exception because validation is True and the value -1 is invalid accordingly to
    # the category 'well-length-with-min-and-max'
    another = scalar_source.CreateCopy()
    assert not another.IsValid()


def testDefaultValue(unit_database_len_pressure) -> None:
    """
    Scalar constructor considers the minimum and maximum values
    when default_value is not defined
    """
    db = unit_database_len_pressure

    db.AddCategory(category="my length", quantity_type="length", min_value=100.0, max_value=200.0)

    # if the default value is not defined, the scalar should not try to set the initial/first
    # value of the new instance to 0.0, but it should assume the minimum allowed value.
    # should not raise ValueError
    length = Scalar("my length")
    assert length.GetValue() == 100.0

    length = Scalar(ObtainQuantity("m", "my length"))
    assert length.GetValue() == 100.0

    length = Scalar(ObtainQuantity("m"))
    assert length.GetValue() == 0.0

    # invalid default value (< min)
    with pytest.raises(AssertionError):
        db.AddCategory(
            category="my pressure",
            quantity_type="pressure",
            default_value=50.0,
            min_value=100.0,
            max_value=200.0,
        )

    # invalid default value (> max)
    with pytest.raises(AssertionError):
        db.AddCategory(
            category="my pressure",
            quantity_type="pressure",
            default_value=300.0,
            min_value=100.0,
            max_value=200.0,
        )

    # invalid default value (<= min)
    with pytest.raises(AssertionError):
        db.AddCategory(
            category="my pressure",
            quantity_type="pressure",
            default_value=100.0,
            min_value=100.0,
            max_value=200.0,
            is_min_exclusive=True,
        )

    # invalid default value (>= min)
    with pytest.raises(AssertionError):
        db.AddCategory(
            category="my pressure",
            quantity_type="pressure",
            default_value=200.0,
            min_value=100.0,
            max_value=200.0,
            is_max_exclusive=True,
        )

    db.AddCategory(
        category="my pressure",
        quantity_type="pressure",
        min_value=100.0,
        max_value=200.0,
        default_value=150.0,
        is_min_exclusive=True,
        is_max_exclusive=False,
    )

    pressure = Scalar("my pressure")
    assert pressure.GetValue() == 150.0

    # default_value not informed. checking if the interval limits are respected
    # when is_min_exclusive or is_max_exclusive have been informed, so the default_value
    # must be
    with pytest.raises(RuntimeError):
        db.AddCategory(
            category="my pressure 2",
            quantity_type="pressure",
            min_value=100.0,
            max_value=200.0,
            is_max_exclusive=True,
        )


def testScalarInvalidValue(unit_database_len_time) -> None:
    db = unit_database_len_time

    db.AddCategory("another-length", "length", min_value=0, max_value=15)

    scalar = Scalar("another-length", value=15, unit="m")
    assert scalar.IsValid()

    scalar = Scalar("another-length", value=-5, unit="m")
    assert not scalar.IsValid()

    # By default the validation will be performed. 15 is a valid value.
    scalar = Scalar("another-length", value=15, unit="m")
    assert scalar.IsValid()

    # Even invalid ,the scalar returns the value, unit and a formatted text.
    another = Scalar("another-length", value=3000, unit="m")
    assert not another.IsValid()
    assert another.GetValue("m") == 3000
    assert another.GetUnit() == "m"
    assert another.GetFormatted() == "3000 [m]"

    # By default the validation will be performed, and in this cases will raise ValueError.
    another_2 = Scalar("another-length", unit="m", value=5000)
    assert not another_2.IsValid()

    # Performing copy between invalid scalars. The validation is not performed on copy.
    copied = another.CreateCopy(unit="cm")
    assert not copied.IsValid()
    assert copied.GetValue("m") == 3000
    assert copied.GetUnit() == "cm"
    assert copied.GetFormatted() == "300000 [cm]"


def testScalarValidUnits(unit_database_empty) -> None:
    db = unit_database_empty
    db.AddUnit("length", "milimeters", "mm", "%f * 1000.0", "%f / 1000.0")
    db.AddUnitBase("length", "meters", "m")
    db.AddUnit("length", "centimeters", "cm", "%f * 100.0", "%f / 100.0")
    db.AddUnit("length", "kilometers", "km", "%f / 1000.0", "%f * 1000.0")
    db.AddCategory("len1", "length", valid_units=["mm", "cm"])
    db.AddCategory("len2", "length", valid_units=["m", "km"])

    s1 = Scalar("len1", 1, "mm")
    assert s1 is not None

    s2 = Scalar("len2", 1, "mm")
    assert s2 is not None


def testScalarRepresentation(unit_database_posc) -> None:
    s1 = Scalar(0.5, "kg/kg", "dimensionless")
    s2 = Scalar(0.125, "g/g", "mass concentration")

    assert s1.GetCategory() == "dimensionless"
    assert s2.GetCategory() == "mass concentration"

    assert repr(s1) == "Scalar(0.5, 'kg/kg', 'dimensionless')"
    assert repr(s2) == "Scalar(0.125, 'g/g', 'mass concentration')"


def testScalarCreationModes() -> None:
    base = Scalar(10, "m")

    assert Scalar("length", 10, "m") == base
    assert Scalar((10.0, "m")) == base

    with pytest.raises(AssertionError):
        Scalar("length", 1.0)  # missing unit


def testScalarPickle(unit_database_posc) -> None:
    import pickle

    simple_scalar = Scalar("length", 10, "m")
    simple_scalar2 = pickle.loads(pickle.dumps(simple_scalar))
    assert simple_scalar == simple_scalar2

    complex_scalar = Scalar("length", 10, "m") * Scalar("time", 5, "s")
    complex_scalar2 = pickle.loads(pickle.dumps(complex_scalar))
    assert complex_scalar == complex_scalar2


def testScalarHashEq() -> None:
    scalar1 = Scalar("length", 10, "m")
    scalar2 = Scalar("length", 10, "m")
    scalar3 = Scalar("length", 10, "cm")
    scalar4 = Scalar("length", scalar2.GetValue("cm"), "cm")

    assert scalar1 == scalar2
    assert hash(scalar1) == hash(scalar2)
    assert hash(scalar1) == hash(scalar1)
    assert scalar2 != scalar3
    assert hash(scalar2) != hash(scalar3)
    assert scalar3 != scalar4
    assert hash(scalar4) != hash(scalar3)


def testChangeScalars() -> None:
    class Fluid:
        density = Scalar(0, "lbm/galUS")
        concentration = Scalar(0, "%")

    fluid = Fluid()
    assert fluid.density.GetValue("lbm/galUS") == 0
    assert fluid.concentration.GetValue("%") == 0

    ChangeScalars(fluid, density=(10, "lbm/galUS"), concentration=(1.0, "%"))

    assert fluid.density.GetValue("lbm/galUS") == 10
    assert fluid.concentration.GetValue("%") == 1.0
