/*
 * Supports various other tests not covered by the tests in
 * ../../grammar/testcases.csv.  These are generally tests of the
 * java-specific parts of the interface, rather than tests of what
 * does and doesn't parse.
 *
 * In a few cases, these duplicate tests in testcases.csv, typically
 * cases which are more clearly edge-cases.  The tests in testcases.csv
 * work through 'bulk' tests, which try to be exhaustive rather than tricky.
! */

import uk.me.nxg.unity.*;

import org.junit.Test;
import static org.junit.Assert.*;

public class TestMisc {

    @Test public void parserSelection() throws Exception {
        for (Syntax stx : Syntax.values()) {
            if (stx == Syntax.ALL
                || stx == Syntax.DEBUG
                || stx == Syntax.LATEX) {
                continue;
            }

            UnitParser p = new UnitParser(stx);
        }
    }

    // Confirm that unitString and toString don't throw any assertions
    // (specifically AssertionError) for any of the existing
    // syntax types apart from ALL.  We don't try catching and
    // reporting these errors: they are reported by the harness in
    // the usual way.
    @Test public void writerSelection() throws Exception {
        UnitParser p = new UnitParser(Syntax.FITS, "m");
        UnitExpr expr = p.getParsed();
        OneUnit u = expr.getUnit(0);

        for (Syntax stx : Syntax.values()) {
            if (stx == Syntax.ALL) continue; // skip this one -- throws exception
            String s = u.unitString(stx);
            s = expr.toString(stx);
        }
    }

    @Test public void testSimpleFitsParse() throws Exception {
        UnitParser p = new UnitParser(Syntax.FITS, "cm/s");
        UnitExpr expr = p.getParsed();

        //System.out.println("cm/s -> " + expr.toDebugString());
        assertEquals("cm s-1", expr.toString());

        // FIXME: We could probably benefit from some more thorough
        // testing of the Dimensions arithmetic.
        Dimensions d = expr.getDimensions();
        @SuppressWarnings("deprecation")
        float[] actualFloatD = d.exponents();

        // The following test depends on us knowing the indexes of the
        // exponents, which isn't public.
        float correctD[] = { 1.0f, 0, -1.0f, 0, 0, 0, 0 };
        assertArrayEquals("array1", correctD, actualFloatD, 0.0f);
        assertEquals("miscpow-length", 1.0f, d.exponent(Dimensions.BaseQuantity.LENGTH), 0);
        assertEquals("miscpow-mass", 0, d.exponent(Dimensions.BaseQuantity.MASS), 0);

        int i = 0;
        for (Dimensions.BaseQuantity q : Dimensions.BaseQuantity.values()) {
            actualFloatD[i++] = d.exponent(q);
        }
        assertArrayEquals("array2", correctD, actualFloatD, 0.0f);

        assertEquals("L T^-1", d.toString());
        assertEquals("unit1", "Metre", expr.getUnit(0).getBaseUnitName());
        assertEquals("factor1", -2, expr.getUnit(0).getPrefix());
        assertEquals("unit2", "Second", expr.getUnit(1).getBaseUnitName());
        assertEquals("factor2", 0, expr.getUnit(1).getPrefix());

        // check we can re-parse
        expr = p.parse("kg.m/ms^2");
        d = expr.getDimensions();
        float correctD2[] = { 1.0f, 1.0f, -2.0f, 0, 0, 0, 0 };
        assertEquals("dim3-length", 1.0f, d.exponent(Dimensions.BaseQuantity.LENGTH), 0);
        assertEquals("dim3-mass", 1.0f, d.exponent(Dimensions.BaseQuantity.MASS), 0);
        assertEquals("dim3-time", -2.0f, d.exponent(Dimensions.BaseQuantity.TIME), 0);
        assertEquals("dim3-current", 0, d.exponent(Dimensions.BaseQuantity.CURRENT), 0);
        assertEquals("unit3", "Gramme", expr.getUnit(0).getBaseUnitName());
        assertEquals("factor3", 3, expr.getUnit(0).getPrefix());

        assertEquals("dim4", 1.0f, d.exponent(Dimensions.BaseQuantity.LENGTH), 0);
        assertEquals("factor4", 0, expr.getUnit(1).getPrefix());

        assertEquals("unit5", "Second", expr.getUnit(2).getBaseUnitName());
        assertEquals("factor5", -3, expr.getUnit(2).getPrefix());
    }

    @Test public void testFitsParse() throws Exception {
        UnitParser p = new UnitParser(Syntax.FITS, "mm^3/(gnu s)");
        UnitExpr expr = p.getParsed();

        // the leading factor is 1.0; log(1.0)=0.0
        assertEquals("logFactor", 0.0, expr.getLogFactor(), 0);

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition metres = udm.lookupUnitDefinition(Syntax.FITS, "m");
        UnitDefinition seconds = udm.lookupUnitDefinition(Syntax.FITS, "s");
        assertNull(udm.lookupUnitDefinition(Syntax.FITS, "gnu"));

        // There are no Candelas in this unit
        UnitDefinition candelasCorrect = udm.lookupUnitDefinition(Syntax.FITS, "cd");
        UnitDefinition candelasWrong = udm.lookupUnitDefinition(Syntax.FITS, "Cd");
        assertNotNull(candelasCorrect);
        assertNull(candelasWrong);
        OneUnit u = expr.getUnit(candelasCorrect);
        assertNull(u);

        // while we're at it, check that getUnit objects to bad calls
        try {
            String test = null;
            OneUnit badU = expr.getUnit(test);
            fail("getUnit(null string) should fail");
        } catch (IllegalArgumentException e) {
            // that's OK
        } catch (Exception e) {
            fail("getUnit(null string) produced the wrong exception: " + e);
        }
        try {
            UnitDefinition test = null;
            OneUnit badU = expr.getUnit(test);
            fail("getUnit(null unit definition) should fail");
        } catch (IllegalArgumentException e) {
            // that's OK
        } catch (Exception e) {
            fail("getUnit(null unit definition) produced the wrong exception: " + e);
        }

        u = expr.getUnit("m");
        assertEquals(u, expr.getUnit(metres));
        assertTrue(u instanceof SimpleUnit); // XXX Is there a better junit test?
        SimpleUnit su = (SimpleUnit)u;

        assertEquals("Metre", u.getBaseUnitName());
        assertEquals("Metre", u.getBaseUnitDefinition().name());
        assertEquals("http://qudt.org/vocab/unit#Meter", su.getBaseUnitDefinition().getURI());
        assertEquals(metres, su.getBaseUnitDefinition());
        assertEquals("m-prefix", -3, su.getPrefix());
        assertEquals("m-power", 3.0, su.getExponent(), 0);

        u = expr.getUnit("s");
        assertEquals(u, expr.getUnit(seconds));

        assertTrue(u instanceof SimpleUnit); // XXX Is there a better junit test?
        su = (SimpleUnit)u;

        assertEquals("Second", su.getBaseUnitName());
        assertEquals("s-prefix", 0, su.getPrefix());
        assertEquals("s-power", -1.0, su.getExponent(), 0);

        assertEquals("mm3 gnu-1 s-1", expr.toString()); // FITS, but the docs don't guarantee this
        assertEquals("mm3 s-1 gnu-1", expr.canonicalize().toString());
        assertEquals("mm3/gnu/s", expr.toString(Syntax.CDS));
    }

    @Test public void testErgsFits() throws Exception {
        // ergs are
        //   permitted but deprecated in fits
        //   permitted and fine in ogip, and
        //   unrecognised in cds
        UnitExpr expr = new UnitParser(Syntax.FITS, "erg").getParsed();
        assertNotNull(expr);
        assertTrue(expr.allUnitsRecognised(Syntax.FITS));
        assertFalse(expr.allUnitsRecommended(Syntax.FITS));
        assertTrue(expr.allUnitsRecognised(Syntax.OGIP));
        assertTrue(expr.allUnitsRecommended(Syntax.OGIP));
        assertFalse(expr.allUnitsRecognised(Syntax.CDS));
        assertFalse(expr.allUnitsRecommended(Syntax.CDS));

        OneUnit erg = expr.getUnit("erg");
        assertTrue(erg.isRecognisedUnit(Syntax.FITS));
        assertFalse(erg.isRecommendedUnit(Syntax.FITS));

        assertTrue(erg.isRecognisedUnit(Syntax.OGIP));
        assertTrue(erg.isRecommendedUnit(Syntax.OGIP));

        assertFalse(erg.isRecognisedUnit(Syntax.CDS));
        assertFalse(erg.isRecommendedUnit(Syntax.CDS));

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition ergs = udm.lookupUnitDefinition(Syntax.FITS, "erg");

        assertEquals(erg, expr.getUnit(ergs));
        assertTrue(erg instanceof SimpleUnit);

        // FITS (which we parsed with) recognised "erg", and so
        // produces the abstract unit name here.
        assertEquals("Erg", erg.getBaseUnitName());
        assertEquals(ergs, erg.getBaseUnitDefinition());

        assertEquals("http://qudt.org/vocab/dimension#Dimension_SI_L2MT-2", expr.getDimensions().getURI());
    }

    @Test public void testErgsCds() throws Exception {
        // Re-do some of the tests of testErgsFits, with a different
        // parsing syntax

        UnitExpr expr = new UnitParser(Syntax.CDS, "erg").getParsed();
        assertNotNull(expr);
        // these are the same tests as in testErgsFits -- these
        // results should be independent of the parsing syntax (yes?)

        // ergs aren't a recognised unit in the CDS syntax,
        // so we shouldn't be able to find any dimensions of the expression
        assertNull(expr.getDimensions()); // deprecated method, but check it anyway

        // we can look it up by string, even though this isn't a known unit
        OneUnit erg = expr.getUnit("erg");
        assertNotNull(erg);
        assertTrue(erg instanceof SimpleUnit);

        assertSame(erg, expr.getUnit(0));

        assertEquals("erg", erg.getBaseUnitName()); // ie (unknown) "erg" not "Erg"
        assertNull(erg.getBaseUnitDefinition()); // not a known unit
        assertFalse(erg.isRecognisedUnit());

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition ergs = udm.lookupUnitDefinition(Syntax.CDS, "erg");
        assertNull(ergs);
    }

    @Test public void testErgsCdsGuessing() throws Exception {
        UnitParser p = new UnitParser(Syntax.CDS);
        p.setGuessing(true);
        UnitExpr expr = p.parse("erg");

        // This is an unknown unit in the CDS syntax, but we should
        // have guessed it correctly.  In that case, it's a recognised
        // unit.
        OneUnit erg = expr.getUnit(0);
        assertFalse(erg.isRecognisedUnit(Syntax.CDS));
        assertFalse(erg.isRecommendedUnit(Syntax.CDS));
        assertTrue(erg.isRecognisedUnit());
        assertTrue(erg.wasGuessed());
        assertEquals("erg", erg.toString());
        assertEquals("Erg", erg.getBaseUnitName());

        assertFalse(expr.allUnitsRecognised(Syntax.CDS));
        assertTrue(expr.allUnitsRecognised());
    }

    @Test public void testVOUnitsQuoted() throws Exception {
        // In the VOUnits syntax, we can quote units to force them to
        // be unrecognised

        UnitParser p = new UnitParser(Syntax.VOUNITS);
        p.setGuessing(false);   // default, but be explicit
        UnitExpr expr = p.parse("'N'"); // quoted 'N' is not a Newton

        OneUnit pseudonewton = expr.getUnit(0);
        assertFalse(pseudonewton.isRecognisedUnit(Syntax.VOUNITS));
        assertFalse(pseudonewton.isRecognisedUnit());

        // this shouldn't start to work when we enable guessing
        p.setGuessing(true);
        expr = p.parse("'N'");

        pseudonewton = expr.getUnit(0);
        assertFalse(pseudonewton.isRecognisedUnit(Syntax.VOUNITS));
        assertFalse(pseudonewton.isRecognisedUnit());
    }

    @Test public void unitInformation() throws Exception {
        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition years = udm.lookupUnitDefinition(Syntax.FITS, "yr");
        assertEquals("http://bitbucket.org/nxg/unity/ns/unit#JulianYear", years.getURI());

        UnitRepresentation yearsRep = udm.lookupUnitRepresentation(Syntax.FITS, years);
        StringBuffer sb = new StringBuffer();

        // The format of the toString output is not documented,
        // so this may change, in principle.
        sb.append(yearsRep.toString());

        while (yearsRep.hasNext()) {
            yearsRep = yearsRep.next();
            sb.append('+').append(yearsRep.toString());
        }
        assertEquals("a+yr", sb.toString());

        UnitDefinition newton = udm.lookupUnitDefinition("http://qudt.org/vocab/unit#Newton");
        assertNotNull(newton);
        assertEquals("Newton", newton.name());
    }

    @Test public void preferredUnits() throws Exception {
        // Check recommended/deprecated status, and preferred output forms
        // FITS: accepts pixel and pix, prefers pixel
        // OGIP: accepts pixel
        // CDS:  accepts pix
        UnitExpr expr = new UnitParser(Syntax.FITS, "pix").getParsed();

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition pixels = udm.lookupUnitDefinition(Syntax.FITS, "pixel");

        OneUnit pix = expr.getUnit("pix");
        assertNotNull(pix);

        assertTrue(pix instanceof SimpleUnit);
        SimpleUnit spix = (SimpleUnit)pix;

        assertEquals(pixels, spix.getBaseUnitDefinition());
        assertEquals(pix, expr.getUnit(pixels));

        assertEquals("pixel", expr.toString(Syntax.FITS));
        assertEquals("pixel", expr.toString(Syntax.OGIP));
        assertEquals("pix",   expr.toString(Syntax.CDS));
    }

    @Test public void unpreferredUnits() throws Exception {
        // FITS recognises both "yr" and "a", but prefers "a"
        UnitExpr exprFits = new UnitParser(Syntax.FITS, "Ga").getParsed();
        // OGIP recognises only "yr"
        UnitExpr exprOgipYr = new UnitParser(Syntax.OGIP, "Gyr").getParsed();
        // This is a billion "a"s -- nothing to do with years
        UnitExpr exprOgipA = new UnitParser(Syntax.OGIP, "Ga").getParsed();

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition years = udm.lookupUnitDefinition(Syntax.FITS, "yr");
        UnitDefinition annums = udm.lookupUnitDefinition(Syntax.FITS, "a");
        assertSame(years, annums);

        UnitDefinition years1 = udm.lookupUnitDefinition(Syntax.OGIP, "yr");
        UnitDefinition annums1 = udm.lookupUnitDefinition(Syntax.OGIP, "a");
        assertSame(years, years1);
        // OGIP doesn't recognise "a" as referring to years
        assertNull(annums1);

        // We can look up the FITS-parsed expression using units and
        // both of the string symbols which FITS permits for years
        OneUnit yearUnit = exprFits.getUnit(years);
        OneUnit yearString = exprFits.getUnit("yr");
        OneUnit annumUnit = exprFits.getUnit(annums);
        OneUnit annumString = exprFits.getUnit("a");
        assertEquals(yearUnit, yearString);
        assertEquals(yearUnit, annumUnit);
        assertEquals(yearUnit, annumString);
        // and see test originalUnitString

        OneUnit ogipYrYearUnit = exprOgipYr.getUnit(years);
        OneUnit ogipYrYearString = exprOgipYr.getUnit("yr");
        OneUnit ogipYrAnnumUnit = exprOgipYr.getUnit(annums);
        OneUnit ogipYrAnnumString = exprOgipYr.getUnit("a");
        assertEquals(yearUnit, ogipYrYearUnit);
        // these two are equal, but not the same object
        assertNotSame(yearUnit, ogipYrYearUnit);
        // but...
        assertSame(yearUnit.getBaseUnitDefinition(), ogipYrYearUnit.getBaseUnitDefinition());
        assertEquals(ogipYrYearUnit, ogipYrYearString);
        assertEquals(ogipYrYearUnit, ogipYrAnnumUnit);
        // OGIP doesn't know about annums, so as far as it's
        // concerned, the "a" unit has nothing to do with "yr", and
        // didn't appear in the original string.
        assertNull(ogipYrAnnumString);

        OneUnit ogipAYearUnit = exprOgipA.getUnit(years);
        OneUnit ogipAYearString = exprOgipA.getUnit("yr");
        OneUnit ogipAAnnumUnit = exprOgipA.getUnit(annums);
        OneUnit ogipAAnnumString = exprOgipA.getUnit("a");
        // OGIP doesn't recognise "a" as year, so none of the
        // following are recognised
        assertNull(ogipAYearUnit);
        assertNull(ogipAYearString);
        assertNull(ogipAAnnumUnit);
        // The ogipAAnnumString is a unit of "a"s, not a unit of years.
        assertNotNull(ogipAAnnumString);
        // but it's not a year, so...
        assertFalse(yearUnit.equals(ogipAAnnumString));

        assertEquals("Julian year", yearUnit.getBaseUnitDefinition().name());
        assertEquals("Julian year", yearUnit.getBaseUnitName());
        assertEquals("Julian year", ogipYrYearUnit.getBaseUnitName());
        assertEquals("a", ogipAAnnumString.getBaseUnitName()); // the mysterious "a" unit
    }

    @Test public void unusualUnits() throws Exception {
        // parse femto-furlong (just "furlong" gets parsed as femto-urlong)
        UnitExpr expr = new UnitParser(Syntax.FITS, "ffurlong").getParsed();

        OneUnit furlong = expr.getUnit("furlong");
        assertNotNull(furlong);

        assertNull(furlong.getBaseUnitDefinition()); // not a known unit

        assertEquals("ffurlong", expr.toString());
    }

    @Test public void guessedUnits() throws Exception {
        // parse various non-recognised but guessable units

        // use the CDS parser, which doesn't recognise ergs, so that
        // we confirm that we can still obtain that by guessing
        UnitParser p = new UnitParser(Syntax.CDS);
        p.setGuessing(true);

        // with degrees/sec, note that degrees does have an SI prefix, and s doesn't
        // (so we exercise two branches)

        UnitExpr expr = p.parse("degrees/s");
        OneUnit u = expr.getUnit(0);
        UnitDefinition d = u.getBaseUnitDefinition();
        assertNotNull(d);
        assertEquals("guessed degrees1", "http://qudt.org/vocab/unit#DegreeAngle", d.getURI());
        assertEquals(0, u.getPrefix());
        assertEquals(1.0, u.getExponent(), 0);
        assertTrue(u.wasGuessed());

        u = expr.getUnit(1);
        assertEquals("guessed seconds1", "http://qudt.org/vocab/unit#SecondTime", u.getBaseUnitDefinition().getURI());
        assertEquals(0, u.getPrefix());
        assertEquals(-1.0, u.getExponent(), 0);
        assertFalse(u.wasGuessed());
        assertEquals("deg s-1", expr.toString());

        // another variant of the same guessable units
        // (but turned upside down, just to check there's no hangover from the previous test)
        expr = p.parse("ksec/degs2");
        u = expr.getUnit(0);
        d = u.getBaseUnitDefinition();
        assertNotNull(d);
        assertEquals("guessed seconds2", "http://qudt.org/vocab/unit#SecondTime", d.getURI());
        assertEquals(3, u.getPrefix());
        assertEquals(1.0, u.getExponent(), 0);
        assertTrue(u.wasGuessed());

        u = expr.getUnit(1);
        d = u.getBaseUnitDefinition();
        assertNotNull(d);
        assertEquals("guessed degrees2", "DegreeAngle", d.label());
        assertEquals(0, u.getPrefix());
        assertEquals(-2.0, u.getExponent(), 0);
        assertTrue(u.wasGuessed());

        // and another, with an unguessable unit
        expr = p.parse("degreesx/seconds"); // trailing characters don't match
        assertNull(expr.getUnit(0).getBaseUnitDefinition());
        // but we should still recognise the second unit
        u = expr.getUnit(1);
        d = u.getBaseUnitDefinition();
        assertNotNull(d);
        assertEquals("guessed seconds3", "SecondTime", d.label());
        assertTrue(u.wasGuessed());

        // this one has an unrecognised unit
        expr = p.parse("ergs/s");
        u = expr.getUnit(0);
        d = u.getBaseUnitDefinition();
        assertNotNull(d);
        assertEquals("Erg", d.label());
        assertTrue(u.wasGuessed());

        assertFalse(expr.allUnitsRecognised(Syntax.FITS));
        assertTrue(expr.allUnitsRecognised(Syntax.FITS, true));
    }

    @Test public void guessedUnitsOptionally() throws Exception {
        // check that we can turn guessing on and off in the re-used parser

        // use the CDS parser, which doesn't recognise ergs, so that
        // we confirm that we can still obtain that by guessing
        UnitParser p = new UnitParser(Syntax.CDS);

        p.setGuessing(false);   // default, but be explicit
        UnitExpr expr = p.parse("erg");
        OneUnit erg = expr.getUnit(0);
        assertFalse(erg.isRecognisedUnit());

        p.setGuessing(true);
        expr = p.parse("erg");
        erg = expr.getUnit(0);
        assertTrue(erg.isRecognisedUnit());

        p.setGuessing(false);   // and turn it back off again
        expr = p.parse("erg");
        erg = expr.getUnit(0);
        assertFalse(erg.isRecognisedUnit());
    }

    @Test public void originalUnitString() throws Exception {
        // FITS recognises both "yr" and "a", but prefers "a"
        UnitParser p = new UnitParser(Syntax.FITS, "Ga");
        UnitExpr exprFitsA = p.parse("Ga");
        UnitExpr exprFitsYr = p.parse("Gyr");

        // Both exprFitsA and exprFitsYr should produced defined units.
        // We've seen in test unpreferredUnits that we can retrieve
        // the 'year' unit in various ways, and that the unit
        // definition is the same in both cases (specifically 'Julian
        // Year').  Now check that we can retrieve the original unit string.
        assertEquals("a", exprFitsA.getUnit(0).getOriginalUnitString());
        assertEquals("yr", exprFitsYr.getUnit(0).getOriginalUnitString());

        p.setGuessing(true);
        UnitExpr degs = p.parse("degrees");
        // we've checked above, in guessedUnits, that this turns into
        // the DegreeAngle; now confirm that we can retrieve the
        // string "degrees", too
        assertEquals("degrees", degs.getUnit(0).getOriginalUnitString());

        degs = p.parse("mdegs"); // again, with prefix
        assertEquals("degs", degs.getUnit(0).getOriginalUnitString());

    }

    @Test(expected=UnitParserException.class) public void emptyUnits() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.FITS, "").getParsed();
    }

    // XXX I'm not at all sure what the following two cases should do.
    // Should "m/m" produce a dimensionless 'unit'?
    // Should "m*m^2" produce m^3?
    // Should "m * mm" produce 10^-3 m^2 ?
    /*
    @Test public void dubiousUnits1() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.FITS, "m*s*m^2").getParsed();

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition metres = udm.lookupUnitDefinition(Syntax.FITS, "m");
        UnitDefinition seconds = udm.lookupUnitDefinition(Syntax.FITS, "s");

        OneUnit u = expr.getUnit(metres);
        assertNotNull(u);
        assertEquals(u, expr.getUnit("m"));
        assertEquals(3.0, u.getExponent(), 0);

        u = expr.getUnit(seconds);
        assertNotNull(u);
        assertEquals(seconds, expr.getUnit("s"));
        assertEquals(1.0, u.getExponent(), 0);

        assertEquals("m^3/s", expr.toString());
    }

    @Test public void dubiousUnits2() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.FITS, "m/m").getParsed();

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition metres = udm.lookupUnitDefinition(Syntax.FITS, "m");

        OneUnit m = expr.getUnit(metres);
        assertNotNull(m);
        assertEquals(m, expr.getUnit("m"));

        assertEquals(0.0, m.getExponent(), 0);
        assertEquals("???", expr.toString());
    }
    */

    @Test public void unitOrdering() throws Exception {
        // These tests aren't an important part of the interface, and
        // the behaviour of the various compareTo methods isn't
        // documented, but they have an impact on, for example, the
        // way that unit expressions are serialised, so should remain
        // consistent.
        UnitExpr km = new UnitParser(Syntax.FITS, "km").getParsed();
        UnitExpr km__1 = new UnitParser(Syntax.FITS, "km-1").getParsed();
        UnitExpr km_2 = new UnitParser(Syntax.FITS, "km+2").getParsed();
        UnitExpr km__2 = new UnitParser(Syntax.FITS, "km-2").getParsed();
        UnitExpr s = new UnitParser(Syntax.FITS, "s").getParsed();
        UnitExpr foo = new UnitParser(Syntax.FITS, "foo").getParsed();
        UnitExpr wibble = new UnitParser(Syntax.FITS, "wibble").getParsed();
        UnitExpr log_km = new UnitParser(Syntax.FITS, "log(km)").getParsed();
        UnitExpr log_s = new UnitParser(Syntax.FITS, "log(s)").getParsed();
        UnitExpr log_foo = new UnitParser(Syntax.FITS, "log(foo)").getParsed();
        UnitExpr log_wibble = new UnitParser(Syntax.FITS, "log(wibble)").getParsed();
        UnitExpr xfn_km = new UnitParser(Syntax.FITS, "xfn(km)").getParsed();
        UnitExpr xfn_s = new UnitParser(Syntax.FITS, "xfn(s)").getParsed();
        UnitExpr xfn_foo = new UnitParser(Syntax.FITS, "xfn(foo)").getParsed();
        UnitExpr xfn_wibble = new UnitParser(Syntax.FITS, "xfn(wibble)").getParsed();

        UnitExpr cmps[] = {
            km, s,
            s, km__1,
            km, km_2,
            km, km__1,
            km__1, km__2,
            km, foo,
            foo, wibble,
            km, log_km,
            km, log_wibble,
            log_km, log_s,
            log_km, log_foo,
            log_foo, log_wibble,
            log_km, xfn_km,
            xfn_km, xfn_s,
            xfn_km, xfn_foo,
            xfn_foo, xfn_wibble,
        };
        for (int i=0; i<cmps.length; i+=2) {
            UnitExpr smaller = cmps[i];
            UnitExpr larger  = cmps[i+1];
            assertTrue(Integer.toString(i/2), smaller.compareTo(larger) < 0);
        }
    }

    @Test public void equalityTests() throws Exception {
        UnitExpr expr1 = new UnitParser(Syntax.VOUNITS, "km.s**-1").getParsed();
        UnitExpr expr2 = new UnitParser(Syntax.CDS, "km/s").getParsed();
        UnitExpr expr3 = new UnitParser(Syntax.OGIP, "s**(-1) km**1").getParsed();

        assertTrue(expr1.equals(expr2));
        assertTrue(expr2.equals(expr3));
        assertTrue(expr3.equals(expr1));

        UnitExpr expr2a = new UnitParser(Syntax.CDS, "km/s/Mpc").getParsed();
        assertFalse(expr2.equals(expr2a));
        assertFalse(expr2a.equals(expr2));

        UnitExpr expr4 = new UnitParser(Syntax.OGIP, "nm/m").getParsed();
        UnitExpr expr5 = new UnitParser(Syntax.FITS, "m-1*nm").getParsed();
        assertTrue(expr4.equals(expr5));
        assertTrue(expr5.equals(expr4));

        UnitExpr expr6 = new UnitParser(Syntax.FITS, "m*log(kg)").getParsed();
        UnitExpr expr7 = new UnitParser(Syntax.CDS, "[kg].m").getParsed();
        assertTrue(expr6.equals(expr7));
        assertTrue(expr7.equals(expr6));
    }

    @Test public void outputInLocale() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.CDS, "0.1nm/s").getParsed();
        String cdsFormatDefault = expr.toString(Syntax.CDS);
        String cdsFormatFR = expr.toString(Syntax.CDS, java.util.Locale.FRENCH);

        assertEquals("0.1nm/s", cdsFormatDefault);
        assertEquals("0,1nm/s", cdsFormatFR);
    }

    @Test public void testCDSfactor() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.CDS, "3.162277x10+5m").getParsed();
        assertEquals("logFactor", 5.5, expr.getLogFactor(), 0.001);

        OneUnit u = expr.getUnit("m");
        assertEquals("Metre", u.getBaseUnitName());
    }

    @Test public void testSmallCDSfactor() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.CDS, "3.162277x10-5m").getParsed();
        assertEquals("logFactor", -4.5, expr.getLogFactor(), 0.001);

        OneUnit u = expr.getUnit("m");
        assertEquals("Metre", u.getBaseUnitName());
    }

    @Test(expected=UnitParserException.class) public void testCDSbadfactor() throws Exception {
        // same as above, but parsing a CDSfactor in a non-CDS syntax
        UnitExpr expr = new UnitParser(Syntax.FITS, "3.162277x10+5m").getParsed();
    }

    @Test(expected=UnitParserException.class) public void testNegativeCDSbadfactor() throws Exception {
        // same as above, but parsing a CDSfactor in a non-CDS syntax
        UnitExpr expr = new UnitParser(Syntax.FITS, "-3.162277x10+5m").getParsed();
    }

    public void testLog() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.FITS, "log(MHz)").getParsed();
        assertEquals("log(MHz)", expr.toString(Syntax.FITS));
        assertEquals("\\si{\\text{\\ensuremath{\\log(\\si{\\mega Hz})}}}", expr.toString(Syntax.LATEX));
        assertEquals("[MHz]", expr.toString(Syntax.CDS));
    }

    @Test public void testLogScale() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.VOUNITS, "log(1e3MHz)").getParsed();
        // we don't really specify how this formats; it should be something like the following
        assertEquals("log(1000.00MHz)", expr.toString(Syntax.VOUNITS));
        assertEquals("\\si{\\text{\\ensuremath{\\log(\\si{1000\\mega Hz})}}}", expr.toString(Syntax.LATEX));
    }

    @Test public void binaryUnits() throws Exception {
        // this is tested in bulk in the ParseAll/testunits.csv cases,
        // but here are some precise tests.
        UnitExpr expr = new UnitParser(Syntax.VOUNITS, "MB.Mibit").getParsed();

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition unitbyte = udm.lookupUnitDefinition(Syntax.VOUNITS, "byte");
        UnitDefinition unitB = udm.lookupUnitDefinition(Syntax.VOUNITS, "B");
        UnitDefinition bit = udm.lookupUnitDefinition(Syntax.VOUNITS, "bit");

        OneUnit us = expr.getUnit("byte");
        OneUnit u = expr.getUnit(unitbyte);
        assertSame(us, u);
        assertSame(unitbyte, u.getBaseUnitDefinition());
        assertSame(unitbyte, unitB); // two equivalent names

        us = expr.getUnit("bit");
        u = expr.getUnit(bit);
        assertSame(us, u);
        assertSame(bit, u.getBaseUnitDefinition());
    }

    private static void testUnitUsage(Syntax s, String exprString,
                                      boolean isRecognised, boolean satisfiesConstraints)
            throws Exception {
        UnitExpr expr = new UnitParser(s, exprString).getParsed();

        OneUnit u = expr.getUnit(0);
        String id = s.toString() + '{' + exprString + '}';
        if (isRecognised)
            assertTrue(id+"recognised", u.isRecognisedUnit(s));
        else
            assertFalse(id+"unrecognised", u.isRecognisedUnit(s));
        if (satisfiesConstraints)
            assertTrue(id+"satisfied", u.satisfiesUsageConstraints(s));
        else
            assertFalse(id+"unsatisfied", u.satisfiesUsageConstraints(s));
    }

    @Test public void testPrefixConstraints() throws Exception {
        // this is tested in bulk in the ParseAll/testunits.csv cases,
        // but here are some precise tests.

        // UnitExpr expr = new UnitParser(Syntax.VOUNITS, "Mm.Mis.MB.Mibit.Gfoo.Gibar").getParsed();

        // Byte is
        //   * recognised in all four syntaxes as 'byte', and
        //     additionally as 'B' in VOUNITS
        //   * may have an SI prefix in FITS, CDS and VOUNITS, but not OGIP.
        //   * may have a binary prefix in VOUNITS only
        testUnitUsage(Syntax.FITS,    "Mbyte", true, true);
        testUnitUsage(Syntax.OGIP,    "Mbyte", true, false);
        testUnitUsage(Syntax.CDS,     "Mbyte", true, true);
        testUnitUsage(Syntax.VOUNITS, "Mbyte", true, true);

        testUnitUsage(Syntax.VOUNITS, "MB", true, true);
        testUnitUsage(Syntax.VOUNITS, "Mibyte", true, true);
        testUnitUsage(Syntax.VOUNITS, "MiB", true, true);
        Syntax others[] = { Syntax.FITS, Syntax.OGIP, Syntax.CDS };
        for (Syntax stx : others) {
            testUnitUsage(stx, "MB", false, true); // 'B' is unrecognised
            testUnitUsage(stx, "MiB", false, true);    // ...so can have any prefixes
            // 'byte' is a recognised unit, but it's not listed as
            // permitting binary prefixes, in these three syntaxes,
            // so this will parse as the unit 'Mibyte'
            // (which has no constraints)
            testUnitUsage(stx, "Mibyte", false, true);
        }

        // Bit is
        //   * recognised in FITS, CDS and VOUNITS, but not OGIP
        //   * allowed SI prefixes in FITS and CDS
        //   * ...and both SI and binary prefixes in VOUNITS
        //   * ...but allowed binary prefixes only in VOUNITS
        testUnitUsage(Syntax.FITS,    "Mbit",  true, true);
        // binary prefix not recognised in FITS, so this is the unit 'Mibit'
        testUnitUsage(Syntax.FITS,    "Mibit", false, true);
        testUnitUsage(Syntax.OGIP,    "Mbit",  false, true);
        testUnitUsage(Syntax.OGIP,    "Mibit", false, true); // no binary prefix
        testUnitUsage(Syntax.CDS,     "Mbit",  true, true);
        testUnitUsage(Syntax.CDS,     "Mibit", false, true); // no binary prefix
        testUnitUsage(Syntax.VOUNITS, "Mbit",  true, true);
        testUnitUsage(Syntax.VOUNITS, "Mibit", true, true); // binary prefix OK

        // the unit 'foo' is unrecognised everywhere, and so can have any prefixes
        testUnitUsage(Syntax.FITS,    "Mfoo",  false, true);
        testUnitUsage(Syntax.FITS,    "Mifoo", false, true);
        testUnitUsage(Syntax.VOUNITS, "Mfoo",  false, true);
        testUnitUsage(Syntax.VOUNITS, "Mifoo", false, true);
    }

    @Test public void testMultifunctions() throws Exception {
        UnitExpr expr = new UnitParser(Syntax.FITS, "m.log(MHz).xfn(boink).log(boo).xfn(s)").getParsed();
        assertEquals("m log(MHz) xfn(boink) log(boo) xfn(s)", expr.toString(Syntax.FITS));
        assertEquals("m log(MHz) log(boo) xfn(s) xfn(boink)", expr.canonicalize().toString(Syntax.FITS));

        assertEquals("http://qudt.org/vocab/dimension#Dimension_SI_L", expr.getDimensions().getURI());

        UnitDefinitionMap udm = UnitDefinitionMap.getInstance();
        UnitDefinition metre = udm.lookupUnitDefinition(Syntax.FITS, "m");
        UnitDefinition second = udm.lookupUnitDefinition(Syntax.FITS, "s");
        UnitDefinition hertz  = udm.lookupUnitDefinition(Syntax.FITS, "Hz");

        OneUnit us = expr.getUnit("m");
        OneUnit u = expr.getUnit(metre);
        assertSame(us, u);
        assertSame(metre, u.getBaseUnitDefinition());

        FunctionDefinitionMap fdm = FunctionDefinitionMap.getInstance();
        FunctionDefinition logFn = fdm.lookupFunctionDefinition(Syntax.FITS, "log");

        // recommended function of recommended unit
        us = expr.getUnit("Hz");
        assertNotNull(us);
        u = expr.getUnit(hertz);
        assertEquals(u, us);
        assertEquals(hertz, u.getBaseUnitDefinition());
        assertEquals("Hertz", u.getBaseUnitName());
        assertTrue(u instanceof FunctionOfUnit);
        FunctionOfUnit fu = (FunctionOfUnit)u;
        assertEquals("log", fu.getFunctionName());
        assertEquals(logFn, fu.getFunctionDefinition());
        assertTrue(u.isRecognisedUnit(Syntax.FITS));
        assertTrue(u.isRecommendedUnit(Syntax.FITS));
        assertTrue(u.satisfiesUsageConstraints(Syntax.FITS));

        // unknown function of unknown unit
        u = expr.getUnit("boink");
        assertNotNull(u);
        assertEquals("boink", u.getBaseUnitName());
        assertTrue(u instanceof FunctionOfUnit);
        fu = (FunctionOfUnit)u;
        assertEquals("xfn", fu.getFunctionName());
        assertEquals(null, fu.getFunctionDefinition());
        assertFalse(u.isRecognisedUnit(Syntax.FITS));
        assertFalse(u.isRecommendedUnit(Syntax.FITS));
        assertTrue(u.satisfiesUsageConstraints(Syntax.FITS));

        // CDS can't serialise this function (it permits only log())
        UnwritableExpression ex;
        {
            final OneUnit lu = u;
            ex = assertThrows(UnwritableExpression.class,
                              () -> lu.unitString(Syntax.CDS));
            ex = assertThrows(UnwritableExpression.class,
                              () -> expr.toString(Syntax.CDS));
        }

        // recommended function of unknown unit
        u = expr.getUnit("boo");
        assertNotNull(u);
        assertEquals("boo", u.getBaseUnitName());
        assertTrue(u instanceof FunctionOfUnit);
        fu = (FunctionOfUnit)u;
        assertEquals("log", fu.getFunctionName());
        assertEquals(logFn, fu.getFunctionDefinition());
        assertFalse(u.isRecognisedUnit(Syntax.FITS));
        assertFalse(u.isRecommendedUnit(Syntax.FITS));
        assertTrue(u.satisfiesUsageConstraints(Syntax.FITS));

        // unknown function of recommended unit
        us = expr.getUnit("s");
        u = expr.getUnit(second);
        assertEquals(u, us);
        assertEquals(second, u.getBaseUnitDefinition());
        assertEquals("Second", u.getBaseUnitName());
        assertTrue(u instanceof FunctionOfUnit);
        fu = (FunctionOfUnit)u;
        assertEquals("xfn", fu.getFunctionName());
        assertEquals(null, fu.getFunctionDefinition());
        assertFalse(u.isRecognisedUnit(Syntax.FITS));
        assertFalse(u.isRecommendedUnit(Syntax.FITS));
        assertTrue(u.satisfiesUsageConstraints(Syntax.FITS));
    }

    @Test public void testDimensionless() throws Exception {
        UnitParser p = new UnitParser(Syntax.VOUNITS, "1");
        UnitExpr ue = p.getParsed();

        assertTrue(ue.equals(UnitExpr.getDimensionlessExpression()));
        // The following is true, but it is not part of the contract of the class.
        //assertTrue(ue == UnitExpr.getDimensionlessExpression());

        assertEquals("1", ue.toString(Syntax.VOUNITS));
        Dimensions d = ue.getDimensions();
        assertEquals(d, Dimensions.unity());
        assertEquals(0.0, d.exponent(Dimensions.BaseQuantity.LENGTH), 0.0); // for example
        // compare it to itself (not very meaningful, but this shouldn't collapse)
        assertEquals(0, ue.compareTo(ue));

        assertEquals(1.0, ue.getFactor(), 0.0);
        assertEquals(0.0, ue.getLogFactor(), 0.0);
        assertEquals(0, ue.size());
        assertThrows(IndexOutOfBoundsException.class,
                     () -> ue.getUnit(1));
        assertNull(ue.getUnit("m"));

        {
            int countIteratedUnits = 0;
            for (OneUnit u : ue) { // test Iterator<OneUnit>
                countIteratedUnits++;
            }
            assertEquals(0, countIteratedUnits);

            java.util.Iterator<OneUnit> ui = ue.sortedIterator();
            assertFalse(ui.hasNext());
            assertThrows(java.util.NoSuchElementException.class,
                         () -> ui.next());
        }

        assertEquals(ue, ue.canonicalize());

        assertTrue(ue.isFullyConformant(Syntax.VOUNITS));
        assertTrue(ue.allUnitsRecognised(Syntax.VOUNITS));
        assertTrue(ue.allUnitsRecommended(Syntax.VOUNITS));
        assertTrue(ue.allUsageConstraintsSatisfied(Syntax.VOUNITS));
    }

    // check that various things that potentially look like 'dimensionless'
    // do in fact parse as they should
    @Test public void testDimensionlessNot() throws Exception {
        UnitParser p = new UnitParser(Syntax.VOUNITS);

        // This starts with "1", and shouldn't be mistaken for that
        UnitExpr ue = p.parse("10**3m");
        assertNotNull(ue);      // ie, valid string
        assertEquals(3.0, ue.getLogFactor(), 0.0);
        assertNotNull(ue.getUnit("m"));

        // VOUnits strings can start with a 'voufloat' token:
        // [1-9][0-9]*(\.[0-9]+)?([eE][+-]?[0-9]+)?
        ue = p.parse("1.0m");
        assertNotNull(ue);
        assertEquals(0.0, ue.getLogFactor(), 0.0);

        ue = p.parse("1e-1m");
        assertNotNull(ue);
        assertEquals(-1.0, ue.getLogFactor(), 0.0);

        ue = p.parse("2m");     // also VOUFLOAT
        assertNotNull(ue);
        assertEquals(0.30103, ue.getLogFactor(), 1e-5);

        // this starts with a LIT10 token, not a float
        ue = p.parse("10m");
        assertNotNull(ue);
        assertEquals(1.0, ue.getLogFactor(), 0.0);

        // this starts with a LIT1 token, not a float
        // (see https://github.com/ivoa-std/VOUnits/issues/8)
        ue = p.parse("1m");
        assertNotNull(ue);
        assertEquals(0.0, ue.getLogFactor(), 0.0);
    }

    public static void main(String args[]) {
        org.junit.runner.JUnitCore.main("TestMisc");
    }
}
