/*
 * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

@file:Suppress("INVISIBLE_MEMBER")
package test.time

import test.TestPlatform
import test.current
import test.numbers.assertAlmostEquals
import kotlin.math.nextDown
import kotlin.math.pow
import kotlin.test.*
import kotlin.time.*
import kotlin.random.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

private val units = DurationUnit.entries

class DurationTest {

    @Test
    fun constructionFromNumber() {
        // nanosecond precision
        val testValues = listOf(0L, 1L, MAX_NANOS) + List(100) { Random.nextLong(0, MAX_NANOS) }
        for (value in testValues) {
            assertEquals(value, value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
            assertEquals(-value, -value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
        }
        // expressible as long nanoseconds but stored as milliseconds
        for (delta in testValues) {
            val value = (MAX_NANOS + 1) + delta
            val expected = value - (value % NANOS_IN_MILLIS)
            assertEquals(expected, value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
            assertEquals(-expected, -value.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds)
        }
        // any int value of small units can always be represented in nanoseconds
        for (unit in units.filter { it <= DurationUnit.SECONDS }) {
            val scale = convertDurationUnitOverflow(1L, unit, DurationUnit.NANOSECONDS)
            repeat(100) {
                val value = Random.nextInt()
                assertEquals(value * scale, value.toDuration(unit).inWholeNanoseconds)
            }
        }

        for (unit in units) {
            val borderValue = convertDurationUnit(MAX_NANOS, DurationUnit.NANOSECONDS, unit)
            val d1 = borderValue.toDuration(unit)
            val d2 = (borderValue + 1).toDuration(unit)
            assertNotEquals(d1, d1 + 1.nanoseconds)
            assertEquals(d2, d2 + 1.nanoseconds)
        }

        assertEquals(Long.MAX_VALUE / 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).inWholeMilliseconds)
        assertEquals(Long.MAX_VALUE / 1000 * 1000, Long.MAX_VALUE.toDuration(DurationUnit.MICROSECONDS).toLong(DurationUnit.MICROSECONDS))

        assertEquals(Duration.INFINITE, (MAX_MILLIS).toDuration(DurationUnit.MILLISECONDS))
        assertEquals(-Duration.INFINITE, (-MAX_MILLIS).toDuration(DurationUnit.MILLISECONDS))

        run {
            val maxNsDouble = MAX_NANOS.toDouble()
            val lessThanMaxDouble = maxNsDouble.nextDown()
            val maxNs = maxNsDouble.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds
            val lessThanMaxNs = lessThanMaxDouble.toDuration(DurationUnit.NANOSECONDS).inWholeNanoseconds
            assertTrue(maxNs > lessThanMaxNs, "$maxNs should be > $lessThanMaxNs")
        }

        assertFailsWith<IllegalArgumentException> { Double.NaN.toDuration(DurationUnit.SECONDS) }
    }

    @Test
    fun equality() {
        val data = listOf<Pair<Double, DurationUnit>>(
            Pair(2.0, DurationUnit.DAYS),
            Pair(2.0, DurationUnit.HOURS),
            Pair(0.25, DurationUnit.MINUTES),
            Pair(1.0, DurationUnit.SECONDS),
            Pair(50.0, DurationUnit.MILLISECONDS),
            Pair(0.3, DurationUnit.MICROSECONDS),
            Pair(20_000_000_000.0, DurationUnit.NANOSECONDS),
            Pair(1.0, DurationUnit.NANOSECONDS)
        )

        for ((value, unit) in data) {
            repeat(10) {
                val d1 = value.toDuration(unit)
                val unit2 = units.random()
                @OptIn(ExperimentalTime::class)
                val value2 = Duration.convert(value, unit, unit2)
                val d2 = value2.toDuration(unit2)
                assertEquals(d1, d2, "$value $unit in $unit2")
                assertEquals(d1.hashCode(), d2.hashCode())

                val d3 = (value2 * 2).toDuration(unit2)
                assertNotEquals(d1, d3, "$value $unit in $unit2")
            }
        }

        run { // invariant Duration.nanoseconds(d.inWholeNanoseconds) == d when whole nanoseconds fits into Long range
            val d1 = (MAX_NANOS + 1).nanoseconds
            val d2 = d1.inWholeNanoseconds.nanoseconds
            assertEquals(d1.inWholeNanoseconds, d2.inWholeNanoseconds)
            assertEquals(d1, d2)
        }
    }

    @Test
    fun comparison() {
        fun assertGreater(d1: Duration, d2: Duration, message: String) {
            assertTrue(d1 > d2, message)
            assertFalse(d1 <= d2, message)
            assertTrue(
                d1.inWholeNanoseconds > d2.inWholeNanoseconds ||
                        d1.inWholeNanoseconds == d2.inWholeNanoseconds && d1.inWholeMilliseconds > d2.inWholeMilliseconds,
                message
            )
        }

        val d4 = Long.MAX_VALUE.nanoseconds
        val d3 = (MAX_NANOS + 1).nanoseconds
        val d2 = MAX_NANOS.nanoseconds
        val d1 = (MAX_NANOS - 1).nanoseconds

        assertGreater(d4, d2, "same sign, different ranges")
        assertGreater(d3, d2, "same sign, different ranges 2")
        assertGreater(d2, d1, "same sign, same range nanos")
        assertGreater(d4, d3, "same sign, same range millis")
        assertGreater(d2, -d3, "different signs, different ranges")
        assertGreater(d3, -d4, "different signs, same ranges")
        assertGreater(d1, -d2, "different signs, same ranges 2")
    }


    @Test
    fun constructionFactoryFunctions() {
        val n1 = Random.nextInt(Int.MAX_VALUE)
        val n2 = Random.nextLong(Long.MAX_VALUE)
        val n3 = Random.nextDouble()

        assertEquals(n1.toDuration(DurationUnit.DAYS), n1.days)
        assertEquals(n2.toDuration(DurationUnit.DAYS), n2.days)
        assertEquals(n3.toDuration(DurationUnit.DAYS), n3.days)

        assertEquals(n1.toDuration(DurationUnit.HOURS), n1.hours)
        assertEquals(n2.toDuration(DurationUnit.HOURS), n2.hours)
        assertEquals(n3.toDuration(DurationUnit.HOURS), n3.hours)

        assertEquals(n1.toDuration(DurationUnit.MINUTES), n1.minutes)
        assertEquals(n2.toDuration(DurationUnit.MINUTES), n2.minutes)
        assertEquals(n3.toDuration(DurationUnit.MINUTES), n3.minutes)

        assertEquals(n1.toDuration(DurationUnit.SECONDS), n1.seconds)
        assertEquals(n2.toDuration(DurationUnit.SECONDS), n2.seconds)
        assertEquals(n3.toDuration(DurationUnit.SECONDS), n3.seconds)

        assertEquals(n1.toDuration(DurationUnit.MILLISECONDS), n1.milliseconds)
        assertEquals(n2.toDuration(DurationUnit.MILLISECONDS), n2.milliseconds)
        assertEquals(n3.toDuration(DurationUnit.MILLISECONDS), n3.milliseconds)

        assertEquals(n1.toDuration(DurationUnit.MICROSECONDS), n1.microseconds)
        assertEquals(n2.toDuration(DurationUnit.MICROSECONDS), n2.microseconds)
        assertEquals(n3.toDuration(DurationUnit.MICROSECONDS), n3.microseconds)

        assertEquals(n1.toDuration(DurationUnit.NANOSECONDS), n1.nanoseconds)
        assertEquals(n2.toDuration(DurationUnit.NANOSECONDS), n2.nanoseconds)
        assertEquals(n3.toDuration(DurationUnit.NANOSECONDS), n3.nanoseconds)
    }

    @Test
    fun conversionToNumber() {
        assertEquals(24, 1.days.inWholeHours)
        assertEquals(0.5, 12.hours.toDouble(DurationUnit.DAYS))
        assertEquals(0, 12.hours.inWholeDays)
        assertEquals(15, 0.25.hours.inWholeMinutes)
        assertEquals(600, 10.minutes.inWholeSeconds)
        assertEquals(500, 0.5.seconds.inWholeMilliseconds)
        assertEquals(50_000, 0.05.seconds.inWholeMicroseconds)
        assertEquals(50_000, 0.05.milliseconds.inWholeNanoseconds)

        assertEquals(365 * 86400 * 1_000_000_000L, 365.days.inWholeNanoseconds)

        assertEquals(0, Duration.ZERO.inWholeNanoseconds)
        assertEquals(0, Duration.ZERO.inWholeMicroseconds)
        assertEquals(0, Duration.ZERO.inWholeMilliseconds)

        assertEquals(10500, 10.5.seconds.inWholeMilliseconds)
        assertEquals(11, 11.5.milliseconds.inWholeMilliseconds)
        assertEquals(-11, (-11.5).milliseconds.inWholeMilliseconds)
        assertEquals(252_000_000, 252.milliseconds.inWholeNanoseconds)
        assertEquals(Long.MAX_VALUE, (365.days * 293).inWholeNanoseconds) // clamping overflowed value

        repeat(100) {
            val value = Random.nextLong(1000)
            val unit = units.random()
            val unit2 = units.random()

            @OptIn(ExperimentalTime::class)
            assertAlmostEquals(Duration.convert(value.toDouble(), unit, unit2), value.toDuration(unit).toDouble(unit2))
        }

        for (unit in units) {
            assertEquals(Long.MAX_VALUE, Duration.INFINITE.toLong(unit))
            assertEquals(Int.MAX_VALUE, Duration.INFINITE.toInt(unit))
            assertEquals(Double.POSITIVE_INFINITY, Duration.INFINITE.toDouble(unit))
            assertEquals(Long.MIN_VALUE, (-Duration.INFINITE).toLong(unit))
            assertEquals(Int.MIN_VALUE, (-Duration.INFINITE).toInt(unit))
            assertEquals(Double.NEGATIVE_INFINITY, (-Duration.INFINITE).toDouble(unit))
        }
    }

    @Test
    fun componentsOfProperSum() {
        repeat(100) {
            val isNsRange = Random.nextBoolean()
            val d = if (isNsRange)
                Random.nextLong(365L * 146)
            else
                Random.nextLong(365L * 150, 365L * 146_000_000)
            val h = Random.nextInt(24)
            val m = Random.nextInt(60)
            val s = Random.nextInt(60)
            val ns = Random.nextInt(1e9.toInt())
            val expectedNs = if (isNsRange) ns else ns - (ns % NANOS_IN_MILLIS)
            (d.days + h.hours + m.minutes + s.seconds + ns.nanoseconds).run {
                toComponents { seconds, nanoseconds ->
                    assertEquals(d * 86400 + h * 3600 + m * 60 + s, seconds)
                    assertEquals(expectedNs, nanoseconds)
                }
                toComponents { minutes, seconds, nanoseconds ->
                    assertEquals(d * 1440 + h * 60 + m, minutes)
                    assertEquals(s, seconds)
                    assertEquals(expectedNs, nanoseconds)
                }
                toComponents { hours, minutes, seconds, nanoseconds ->
                    assertEquals(d * 24 + h, hours)
                    assertEquals(m, minutes)
                    assertEquals(s, seconds)
                    assertEquals(expectedNs, nanoseconds)
                }
                toComponents { days, hours, minutes, seconds, nanoseconds ->
                    assertEquals(d, days)
                    assertEquals(h, hours)
                    assertEquals(m, minutes)
                    assertEquals(s, seconds)
                    assertEquals(expectedNs, nanoseconds)
                }
            }
        }
    }

    @Test
    fun componentsOfCarriedSum() {
        (36.hours + 90.minutes + 90.seconds + 1500.milliseconds).run {
            toComponents { days, hours, minutes, seconds, nanoseconds ->
                assertEquals(1, days)
                assertEquals(13, hours)
                assertEquals(31, minutes)
                assertEquals(31, seconds)
                assertEquals(500_000_000, nanoseconds)
            }
        }
    }

    @Test
    fun componentsOfInfinity() {
        for (d in listOf(Duration.INFINITE, -Duration.INFINITE)) {
            val expected = if (d.isPositive()) Long.MAX_VALUE else Long.MIN_VALUE
            d.toComponents { seconds, nanoseconds ->
                assertEquals(expected, seconds)
                assertEquals(0, nanoseconds)
            }
            d.toComponents { minutes: Long, seconds: Int, nanoseconds: Int ->
                assertEquals(expected, minutes)
                assertEquals(0, seconds)
                assertEquals(0, nanoseconds)
            }
            d.toComponents { hours, minutes, seconds, nanoseconds ->
                assertEquals(expected, hours)
                assertEquals(0, minutes)
                assertEquals(0, seconds)
                assertEquals(0, nanoseconds)
            }
            d.toComponents { days, hours, minutes, seconds, nanoseconds ->
                assertEquals(expected, days)
                assertEquals(0, hours)
                assertEquals(0, minutes)
                assertEquals(0, seconds)
                assertEquals(0, nanoseconds)
            }
        }
    }

    @Test
    fun infinite() {
        assertTrue(Duration.INFINITE.isInfinite())
        assertTrue((-Duration.INFINITE).isInfinite())
        assertTrue(Double.POSITIVE_INFINITY.nanoseconds.isInfinite())

        // seconds converted to nanoseconds overflow to infinite
        assertTrue(Double.MAX_VALUE.seconds.isInfinite())
        assertTrue((-Double.MAX_VALUE).seconds.isInfinite())
    }


    @Test
    fun negation() {
        repeat(100) {
            val value = Random.nextLong()
            val unit = units.random()

            assertEquals((-value).toDuration(unit), -value.toDuration(unit))
        }
    }

    @Test
    fun signAndAbsoluteValue() {
        val negative = -1.seconds
        val positive = 1.seconds
        val zero = Duration.ZERO

        assertTrue(negative.isNegative())
        assertFalse(zero.isNegative())
        assertFalse(positive.isNegative())

        assertFalse(negative.isPositive())
        assertFalse(zero.isPositive())
        assertTrue(positive.isPositive())

        assertEquals(positive, negative.absoluteValue)
        assertEquals(positive, positive.absoluteValue)
        assertEquals(zero, zero.absoluteValue)
    }

    @Test
    fun negativeZero() {
        fun equivalentToZero(value: Duration) {
            assertEquals(Duration.ZERO, value)
            assertEquals(Duration.ZERO, value.absoluteValue)
            assertEquals(value, value.absoluteValue)
            assertEquals(value, value.absoluteValue)
            assertFalse(value.isNegative())
            assertFalse(value.isPositive())
            assertEquals(Duration.ZERO.toString(), value.toString())
            assertEquals(Duration.ZERO.toIsoString(), value.toIsoString())
            assertEquals(Duration.ZERO.toDouble(DurationUnit.SECONDS), value.toDouble(DurationUnit.SECONDS))
            assertEquals(0, Duration.ZERO.compareTo(value))
            assertEquals(0, Duration.ZERO.toDouble(DurationUnit.NANOSECONDS).compareTo(value.toDouble(DurationUnit.NANOSECONDS)))
        }
        equivalentToZero((-0.0).seconds)
        equivalentToZero((-0.0).toDuration(DurationUnit.DAYS))
        equivalentToZero(-Duration.ZERO)
        equivalentToZero((-1).seconds / Double.POSITIVE_INFINITY)
        equivalentToZero(0.seconds / -1)
        equivalentToZero((-1).seconds * 0.0)
        equivalentToZero(0.seconds * -1)
    }


    @Test
    fun addition() {
        assertEquals(1.5.hours, 1.hours + 30.minutes)
        assertEquals(0.5.days, 6.hours + 360.minutes)
        assertEquals(0.5.seconds, 200.milliseconds + 300_000.microseconds)

        for (value in listOf(Duration.ZERO, 1.nanoseconds, (500 * 365).days)) {
            for (inf in listOf(Duration.INFINITE, -Duration.INFINITE)) {
                for (result in listOf(inf + inf, inf + value, inf + (-value), value + inf, (-value) + inf)) {
                    assertEquals(inf, result)
                }
            }
        }

        assertFailsWith<IllegalArgumentException> { Duration.INFINITE + (-Duration.INFINITE) }
    }

    @Test
    fun subtraction() {
        assertEquals(10.hours, 0.5.days - 120.minutes)
        assertEquals(850.milliseconds, 1.seconds - 150.milliseconds)
        assertEquals(1150.milliseconds, 1.seconds - (-150).milliseconds)
        assertEquals(1.milliseconds, Long.MAX_VALUE.microseconds - (Long.MAX_VALUE - 1_000).microseconds)
        assertEquals((-1).milliseconds, (Long.MAX_VALUE - 1_000).microseconds - Long.MAX_VALUE.microseconds)

        run {
            val offset = 2L * NANOS_IN_MILLIS
            val value = MAX_NANOS + offset
            val base = value.nanoseconds
            val baseNs = base.inWholeMilliseconds * NANOS_IN_MILLIS
            assertEquals(baseNs, base.inWholeNanoseconds)  // base stored as millis

            val smallDeltas = listOf(1L, 2L, 1000L, NANOS_IN_MILLIS - 1L) + List(10) { Random.nextLong(NANOS_IN_MILLIS.toLong()) }
            for (smallDeltaNs in smallDeltas) {
                assertEquals(base, base - smallDeltaNs.nanoseconds, "delta: $smallDeltaNs")
            }

            val deltas = listOf(offset + 1L, offset + 1500L) +
                    List(10) { Random.nextLong(offset + 1500, offset + 10000) } +
                    List(100) { Random.nextLong(offset + 1500, MAX_NANOS) }
            for (deltaNs in deltas) {
                val delta = deltaNs.nanoseconds
                assertEquals(deltaNs, delta.inWholeNanoseconds)
                assertEquals(baseNs - deltaNs, (base - delta).inWholeNanoseconds, "base: $baseNs, delta: $deltaNs")
            }
        }

        for (value in listOf(Duration.ZERO, 1.nanoseconds, (500 * 365).days)) {
            for (inf in listOf(Duration.INFINITE, -Duration.INFINITE)) {
                for (result in listOf(inf - (-inf), inf - value, inf - (-value), value - (-inf), (-value) - (-inf))) {
                    assertEquals(inf, result)
                }
            }
        }

        assertFailsWith<IllegalArgumentException> { Duration.INFINITE - Duration.INFINITE }
    }

    @Test
    fun multiplication() {
        assertEquals(1.days, 12.hours * 2)
        assertEquals(1.days, 60.minutes * 24.0)
        assertEquals(1.microseconds, 20.nanoseconds * 50)

        assertEquals(1.days, 2 * 12.hours)
        assertEquals(12.5.hours, 12.5 * 60.minutes)
        assertEquals(1.microseconds, 50 * 20.nanoseconds)

        assertEquals(Duration.ZERO, 0 * 1.hours)
        assertEquals(Duration.ZERO, 1.seconds * 0.0)

        run { // promoting nanos range to millis range after multiplication
            val value = MAX_NANOS
            assertEquals(value, (value.nanoseconds * 1_000_000).inWholeMilliseconds)
            assertEquals(value / 1000, (value.nanoseconds * 1_000).inWholeMilliseconds)
            assertEquals(Duration.INFINITE, (Long.MAX_VALUE / 1000 + 1).nanoseconds * 1_000_000_000)
        }

        run {
            val value = MAX_NANOS / Int.MAX_VALUE
            assertTrue((value.nanoseconds * Int.MIN_VALUE).inWholeNanoseconds < -MAX_NANOS)
        }

        assertEquals(Duration.INFINITE, Int.MAX_VALUE.days * Int.MAX_VALUE)
        assertEquals(-Duration.INFINITE, Int.MAX_VALUE.days * Int.MIN_VALUE)

        assertEquals(Duration.INFINITE, Duration.INFINITE * Double.POSITIVE_INFINITY)
        assertEquals(Duration.INFINITE, Duration.INFINITE * Double.MIN_VALUE)
        assertEquals(-Duration.INFINITE, Duration.INFINITE * Double.NEGATIVE_INFINITY)
        assertEquals(-Duration.INFINITE, Duration.INFINITE * -1)

        assertFailsWith<IllegalArgumentException> { Duration.INFINITE * 0 }
        assertFailsWith<IllegalArgumentException> { 0.0 * Duration.INFINITE }
    }

    @Test
    fun divisionByNumber() {
        assertEquals(12.hours, 1.days / 2)
        assertEquals(60.minutes, 1.days / 24.0)
        assertEquals(20.seconds, 2.minutes / 6)
        assertEquals(365.days, (365 * 299).days / 299)
        assertEquals(365.days, (365 * 299.5).days / 299.5)

        run {
            val value = MAX_NANOS
            assertEquals(value, (value.milliseconds / 1_000_000).inWholeNanoseconds)
        }

        assertEquals(Duration.INFINITE, 1.seconds / 0)
        assertEquals(-Duration.INFINITE, -1.seconds / 0.0)
        assertEquals(Duration.INFINITE, -1.seconds / (-0.0))

        assertEquals(Duration.INFINITE, Duration.INFINITE / Int.MAX_VALUE)
        assertEquals(Duration.INFINITE, -Duration.INFINITE / Int.MIN_VALUE)
        assertEquals(-Duration.INFINITE, Duration.INFINITE / -1)
        assertEquals(Duration.INFINITE, Duration.INFINITE / Double.MAX_VALUE)

        assertFailsWith<IllegalArgumentException> { Duration.INFINITE / Double.POSITIVE_INFINITY }
        assertFailsWith<IllegalArgumentException> { Duration.ZERO / 0 }
        assertFailsWith<IllegalArgumentException> { Duration.ZERO / 0.0 }
    }

    @Test
    fun divisionByDuration() {
        assertEquals(24.0, 1.days / 1.hours)
        assertEquals(0.1, 9.minutes / 1.5.hours)
        assertEquals(50.0, 1.microseconds / 20.nanoseconds)
        assertEquals(299.0, (365 * 299).days / 365.days)

        assertTrue((Duration.INFINITE / Duration.INFINITE).isNaN())
        assertTrue((Duration.ZERO / Duration.ZERO).isNaN())
    }

    @Test
    fun truncation() {
        fun expect(expected: Duration, value: Duration, unit: DurationUnit) {
            assertEquals(expected, value.truncateTo(unit))
            assertEquals(-expected, (-value).truncateTo(unit))
        }
        for (unit in units) {
            expect(Duration.ZERO, Duration.ZERO, unit)
            expect(Duration.INFINITE, Duration.INFINITE, unit)
            expect(Duration.ZERO, 1.toDuration(unit) - 1.nanoseconds, unit)
            repeat(100) {
                val whole = Random.nextInt(100_000).toDuration(unit)
                expect(whole, whole, unit)
                if (unit > DurationUnit.NANOSECONDS) {
                    val part = Random.nextLong(1, 1.toDuration(unit).inWholeNanoseconds).nanoseconds
                    expect(Duration.ZERO, part, unit)
                    expect(whole, whole + part, unit)
                }
            }
        }
        repeat(10) {
            val d = Random.nextLong().nanoseconds
            expect(d, d, DurationUnit.NANOSECONDS)
        }
        expect(12.microseconds, 12998.nanoseconds, DurationUnit.MICROSECONDS)
        expect(1503.milliseconds, 1503_889_404.nanoseconds, DurationUnit.MILLISECONDS)
        expect(340.seconds, 340_990_567_444L.nanoseconds, DurationUnit.SECONDS)
        expect(3.minutes, 200.seconds, DurationUnit.MINUTES)
        expect(4.hours, 250.minutes, DurationUnit.HOURS)
        expect(1.days, 30.hours, DurationUnit.DAYS)

        // big durations
        run {
            val d = (Long.MAX_VALUE / 4).milliseconds
            for (unit in units) {
                if (unit <= DurationUnit.MILLISECONDS) {
                    expect(d, d, unit)
                } else {
                    expect(d.toLong(unit).toDuration(unit), d, unit)
                }
            }
        }
    }

    @Test
    fun parseAndFormatIsoString() {
        fun test(duration: Duration, vararg isoStrings: String) {
            assertEquals(isoStrings.first(), duration.toIsoString())
            for (isoString in isoStrings) {
                assertEquals(duration, Duration.parseIsoString(isoString), isoString)
                assertEquals(duration, Duration.parse(isoString), isoString)
                assertEquals(duration, Duration.parseIsoStringOrNull(isoString), isoString)
                assertEquals(duration, Duration.parseOrNull(isoString), isoString)
            }
        }

        // zero
        test(Duration.ZERO, "PT0S", "P0D", "PT0H", "PT0M", "P0DT0H", "PT0H0M", "PT0H0S", "PT000000000000000000000000H")

        // single unit
        test(1.days, "PT24H", "P1D", "PT1440M", "PT86400S")
        test(1.hours, "PT1H")
        test(1.minutes, "PT1M")
        test(1.seconds, "PT1S", "PT000000000000000000000001S")
        test(1.milliseconds, "PT0.001S")
        test(1.microseconds, "PT0.000001S")
        test(1.nanoseconds, "PT0.000000001S", "PT0.0000000009S")
        test(0.9.nanoseconds, "PT0.000000001S")

        // rounded to zero
        test(0.1.nanoseconds, "PT0S")
        test(Duration.ZERO, "PT0S", "PT0.0000000004S")

        // several units combined
        test(1.days + 1.minutes, "PT24H1M")
        test(1.days + 1.seconds, "PT24H0M1S")
        test(1.days + 1.milliseconds, "PT24H0M0.001S")
        test(1.hours + 30.minutes, "PT1H30M")
        test(1.hours + 500.milliseconds, "PT1H0M0.500S")
        test(2.minutes + 500.milliseconds, "PT2M0.500S")
        test(90_500.milliseconds, "PT1M30.500S")

        // with sign
        test(-1.days + 15.minutes, "-PT23H45M", "PT-23H-45M", "+PT-24H+15M")
        test(-1.days - 15.minutes, "-PT24H15M", "PT-24H-15M", "-PT25H-45M")
        test(Duration.ZERO, "PT0S", "P1DT-24H", "+PT-1H+60M", "-PT1M-60S", "PT-000000000000000000000000H")

        // infinite
        test(Duration.INFINITE, "PT9999999999999H", "PT+10000000000000H", "-PT-9999999999999H", "-PT-1234567890123456789012S", "PT+000000000000000001234567890123456789012H")
        test(-Duration.INFINITE, "-PT9999999999999H", "-PT10000000000000H", "PT-1234567890123456789012S", "PT-000000000000000001234567890123456789012H")
    }

    @Test
    fun parseIsoStringFailing() {
        for (invalidValue in listOf(
            "", " ", "P", "PT", "P1DT", "P1", "PT1", "0", "+P", "+", "-", "h", "H", "something",
            "1m", "1d", "2d 11s", "Infinity", "-Infinity",
            "P+12+34D", "P12-34D", "PT1234567890-1234567890S",
            " P1D", "PT1S ",
            "P3W",
            "P1Y", "P1M", "P1S", "PT1D", "PT1Y",
            "PT1S2S", "PT1S2H",
            "P9999999999999DT-9999999999999H",
            "PT1.5H", "PT0.5D", "PT.5S", "PT0.25.25S",
            "PT+-2H", "PT-+2H", "PT+-01234567890123456S"
        )) {
            assertNull(Duration.parseIsoStringOrNull(invalidValue), invalidValue)
            assertFailsWith<IllegalArgumentException>(invalidValue) { Duration.parseIsoString(invalidValue) }.let { e ->
                assertContains(e.message!!, "'$invalidValue'")
            }
        }

    }

    @Test
    fun parseAndFormatInUnits() {
        if (TestPlatform.current == TestPlatform.WasmWasi) return

        var d = 1.days + 15.hours + 31.minutes + 45.seconds +
                678.milliseconds + 920.microseconds + 516.34.nanoseconds

        fun test(unit: DurationUnit, vararg representations: String) {
            assertFails { d.toString(unit, -1) }
            assertEquals(representations.toList(), representations.indices.map { d.toString(unit, it) })
            for ((decimals, string) in representations.withIndex()) {
                val d1 = Duration.parse(string)
                assertEquals(d1, Duration.parseOrNull(string))
                if (!(d1 == d || (d1 - d).absoluteValue <= (0.5 * 10.0.pow(-decimals)).toDuration(unit))) {
                    fail("Parsed value $d1 (from $string) is too far from the real value $d")
                }
            }
        }

        test(DurationUnit.DAYS, "2d", "1.6d", "1.65d", "1.647d")
        test(DurationUnit.HOURS, "40h", "39.5h", "39.53h")
        test(DurationUnit.MINUTES, "2372m", "2371.8m", "2371.76m")
        d -= 39.hours
        test(DurationUnit.SECONDS, "1906s", "1905.7s", "1905.68s", "1905.679s")
        d -= 1904.seconds
        test(DurationUnit.MILLISECONDS, "1679ms", "1678.9ms", "1678.92ms", "1678.921ms")
        d -= 1678.milliseconds
        test(DurationUnit.MICROSECONDS, "921us", "920.5us", "920.52us", "920.516us")
        d -= 920.microseconds
        // no sub-nanosecond precision
        test(DurationUnit.NANOSECONDS, "516ns", "516.0ns", "516.00ns", "516.000ns", "516.0000ns")
        d = (d - 516.nanoseconds) / 17
        test(DurationUnit.NANOSECONDS, "0ns", "0.0ns", "0.00ns", "0.000ns", "0.0000ns")

        // infinite
//        d = Duration.nanoseconds(Double.MAX_VALUE)
//        test(DurationUnit.DAYS, "2.08e+294d")
//        test(DurationUnit.NANOSECONDS, "1.80e+308ns")

        assertEquals("0.500000000000s", 0.5.seconds.toString(DurationUnit.SECONDS, 100))
        assertEquals("99999000000000.000000000000ns", 99_999.seconds.toString(DurationUnit.NANOSECONDS, 15))
        assertContains(
            listOf(
                "-4611686018427388000000000.000000000000ns",
                "-4611686018427387904000000.000000000000ns"
            ),
            (-(MAX_MILLIS - 1).milliseconds).toString(DurationUnit.NANOSECONDS, 15)
        )

        d = Duration.INFINITE
        test(DurationUnit.DAYS, "Infinity", "Infinity")
        d = -Duration.INFINITE
        test(DurationUnit.NANOSECONDS, "-Infinity", "-Infinity")
    }


    @Test
    fun parseAndFormatDefault() {
        fun testParsing(string: String, expectedDuration: Duration) {
            assertEquals(expectedDuration, Duration.parse(string), string)
            assertEquals(expectedDuration, Duration.parseOrNull(string), string)
        }

        fun test(duration: Duration, vararg expected: String) {
            val actual = duration.toString()
            assertEquals(expected.first(), actual)

            if (duration.isPositive()) {
                if (' ' in actual) {
                    assertEquals("-($actual)", (-duration).toString())
                } else {
                    assertEquals("-$actual", (-duration).toString())
                }
            }

            for (string in expected) {
                testParsing(string, duration)
                if (duration.isPositive() && duration.isFinite()) {
                    testParsing("+($string)", duration)
                    testParsing("-($string)", -duration)
                    if (' ' !in string) {
                        testParsing("+$string", duration)
                        testParsing("-$string", -duration)
                    }
                }
            }
        }

        test(101.days, "101d", "2424h")
        test(45.3.days, "45d 7h 12m", "45.3d", "45d 7.2h") // 0.3d == 7.2h
        test(45.days, "45d")

        test(40.5.days, "40d 12h", "40.5d", "40d 720m")
        test(40.days + 20.minutes, "40d 0h 20m", "40d 20m", "40d 1200s")
        test(40.days + 20.seconds, "40d 0h 0m 20s", "40d 20s")
        test(40.days + 100.nanoseconds, "40d 0h 0m 0.000000100s", "40d 100ns")

        test(40.hours + 15.minutes, "1d 16h 15m", "40h 15m")
        test(40.hours, "1d 16h", "40h")

        test(12.5.hours, "12h 30m")
        test(12.hours + 15.seconds, "12h 0m 15s")
        test(12.hours + 1.nanoseconds, "12h 0m 0.000000001s")
        test(30.minutes, "30m")
        test(17.5.minutes, "17m 30s")

        test(16.5.minutes, "16m 30s")
        test(1097.1.seconds, "18m 17.1s")
        test(90.36.seconds, "1m 30.36s")
        test(50.seconds, "50s")
        test(1.3.seconds, "1.3s")
        test(1.seconds, "1s")

        test(0.5.seconds, "500ms")
        test(40.2.milliseconds, "40.2ms")
        test(4.225.milliseconds, "4.225ms")
        test(4.24501.milliseconds, "4.245010ms", "4ms 245us 10ns")
        test(1.milliseconds, "1ms")

        test(0.75.milliseconds, "750us")
        test(75.35.microseconds, "75.35us")
        test(7.25.microseconds, "7.25us")
        test(1.035.microseconds, "1.035us")
        test(1.005.microseconds, "1.005us")
        test(1800.nanoseconds, "1.8us", "1800ns", "0.0000000005h")

        test(950.5.nanoseconds, "951ns")
        test(85.23.nanoseconds, "85ns")
        test(8.235.nanoseconds, "8ns")
        test(1.nanoseconds, "1ns", "0.9ns", "0.001us", "0.0009us")
        test(1.3.nanoseconds, "1ns")
        test(0.75.nanoseconds, "1ns")
        test(0.7512.nanoseconds, "1ns")

        // equal to zero
//        test(0.023.nanoseconds, "0.023ns")
//        test(0.0034.nanoseconds, "0.0034ns")
//        test(0.0000035.nanoseconds, "0.0000035ns")

        test(Duration.ZERO, "0s", "0.4ns", "0000.0000ns")
        test(365.days * 10000, "3650000d")
        test(300.days * 100000, "30000000d")
        test(365.days * 100000, "36500000d")
        test((MAX_MILLIS - 1).milliseconds, "53375995583d 15h 36m 27.902s") // max finite value

        // all infinite
//        val universeAge = Duration.days(365.25) * 13.799e9
//        val planckTime = Duration.seconds(5.4e-44)

//        test(universeAge, "5.04e+12d")
//        test(planckTime, "5.40e-44s")
//        test(Duration.nanoseconds(Double.MAX_VALUE), "2.08e+294d")
        test(Duration.INFINITE, "Infinity", "53375995583d 20h", "+Infinity")
        test(-Duration.INFINITE, "-Infinity", "-(53375995583d 20h)")
    }

    @Test
    fun parseDefaultFailing() {
        for (invalidValue in listOf(
            "", " ", "P", "PT", "P1DT", "P1", "PT1", "0", "+P", "+", "-", "h", "H", "something",
            "1234567890123456789012ns", "Inf", "-Infinity value",
            "1s ", " 1s",
            "1d 1m 1h", "1s 2s",
            "-12m 15s", "-12m -15s", "-()", "-(12m 30s",
            "+12m 15s", "+12m +15s", "+()", "+(12m 30s",
            "()", "(12m 30s)",
            "12.5m 11.5s", ".2s", "0.1553.39m",
            "P+12+34D", "P12-34D", "PT1234567890-1234567890S",
            " P1D", "PT1S ",
            "P1Y", "P1M", "P1S", "PT1D", "PT1Y",
            "PT1S2S", "PT1S2H",
            "P9999999999999DT-9999999999999H",
            "PT1.5H", "PT0.5D", "PT.5S", "PT0.25.25S",
        )) {
            assertNull(Duration.parseOrNull(invalidValue), invalidValue)
            assertFailsWith<IllegalArgumentException>(invalidValue) { Duration.parse(invalidValue) }.let { e ->
                assertContains(e.message!!, "'$invalidValue'")
            }
        }
    }

}
