<?php
/** @license MIT
 * Copyright 2017 J. King, Dustin Wilson et al.
 * See LICENSE and AUTHORS files for details */

declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Misc;

use JKingWeb\Arsse\ExceptionType;
use JKingWeb\Arsse\Misc\ValueInfo as I;
use JKingWeb\Arsse\Test\Misc\StrClass;
use JKingWeb\Arsse\Test\Result;

/** @covers \JKingWeb\Arsse\Misc\ValueInfo */
class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest {
    public function setUp() {
        self::clearData();
    }

    public function testGetIntegerInfo() {
        $tests = [
            [null,          I::NULL],
            ["",            I::NULL],
            [1,             I::VALID],
            [PHP_INT_MAX,   I::VALID],
            [1.0,           I::VALID],
            ["1.0",         I::VALID],
            ["001.0",       I::VALID],
            ["1.0e2",       I::VALID],
            ["1",           I::VALID],
            ["001",         I::VALID],
            ["1e2",         I::VALID],
            ["+1.0",        I::VALID],
            ["+001.0",      I::VALID],
            ["+1.0e2",      I::VALID],
            ["+1",          I::VALID],
            ["+001",        I::VALID],
            ["+1e2",        I::VALID],
            [0,             I::VALID | I::ZERO],
            ["0",           I::VALID | I::ZERO],
            ["000",         I::VALID | I::ZERO],
            [0.0,           I::VALID | I::ZERO],
            ["0.0",         I::VALID | I::ZERO],
            ["000.000",     I::VALID | I::ZERO],
            ["+0",          I::VALID | I::ZERO],
            ["+000",        I::VALID | I::ZERO],
            ["+0.0",        I::VALID | I::ZERO],
            ["+000.000",    I::VALID | I::ZERO],
            [-1,            I::VALID | I::NEG],
            [-1.0,          I::VALID | I::NEG],
            ["-1.0",        I::VALID | I::NEG],
            ["-001.0",      I::VALID | I::NEG],
            ["-1.0e2",      I::VALID | I::NEG],
            ["-1",          I::VALID | I::NEG],
            ["-001",        I::VALID | I::NEG],
            ["-1e2",        I::VALID | I::NEG],
            [-0,            I::VALID | I::ZERO],
            ["-0",          I::VALID | I::ZERO],
            ["-000",        I::VALID | I::ZERO],
            [-0.0,          I::VALID | I::ZERO],
            ["-0.0",        I::VALID | I::ZERO],
            ["-000.000",    I::VALID | I::ZERO],
            [false,         0],
            [true,          0],
            ["on",          0],
            ["off",         0],
            ["yes",         0],
            ["no",          0],
            ["true",        0],
            ["false",       0],
            [INF,           I::FLOAT],
            [-INF,          I::FLOAT | I::NEG],
            [NAN,           I::FLOAT],
            [[],            0],
            ["some string", 0],
            ["           ", 0],
            [new \StdClass, 0],
            [new StrClass(""),    I::NULL],
            [new StrClass("1"),   I::VALID],
            [new StrClass("0"),   I::VALID | I::ZERO],
            [new StrClass("-1"),  I::VALID | I::NEG],
            [new StrClass("Msg"), 0],
            [new StrClass("   "), 0],
            [2.5,           I::FLOAT],
            [0.5,           I::FLOAT],
            ["2.5",         I::FLOAT],
            ["0.5",         I::FLOAT],
            [" 1 ",         I::VALID],
        ];
        foreach ($tests as $test) {
            list($value, $exp) = $test;
            $this->assertSame($exp, I::int($value), "Test returned ".decbin(I::int($value))." for value: ".var_export($value, true));
        }
    }
    public function testGetStringInfo() {
        $tests = [
            [null,          I::NULL],
            ["",            I::VALID | I::EMPTY],
            [1,             I::VALID],
            [PHP_INT_MAX,   I::VALID],
            [1.0,           I::VALID],
            ["1.0",         I::VALID],
            ["001.0",       I::VALID],
            ["1.0e2",       I::VALID],
            ["1",           I::VALID],
            ["001",         I::VALID],
            ["1e2",         I::VALID],
            ["+1.0",        I::VALID],
            ["+001.0",      I::VALID],
            ["+1.0e2",      I::VALID],
            ["+1",          I::VALID],
            ["+001",        I::VALID],
            ["+1e2",        I::VALID],
            [0,             I::VALID],
            ["0",           I::VALID],
            ["000",         I::VALID],
            [0.0,           I::VALID],
            ["0.0",         I::VALID],
            ["000.000",     I::VALID],
            ["+0",          I::VALID],
            ["+000",        I::VALID],
            ["+0.0",        I::VALID],
            ["+000.000",    I::VALID],
            [-1,            I::VALID],
            [-1.0,          I::VALID],
            ["-1.0",        I::VALID],
            ["-001.0",      I::VALID],
            ["-1.0e2",      I::VALID],
            ["-1",          I::VALID],
            ["-001",        I::VALID],
            ["-1e2",        I::VALID],
            [-0,            I::VALID],
            ["-0",          I::VALID],
            ["-000",        I::VALID],
            [-0.0,          I::VALID],
            ["-0.0",        I::VALID],
            ["-000.000",    I::VALID],
            [false,         0],
            [true,          0],
            ["on",          I::VALID],
            ["off",         I::VALID],
            ["yes",         I::VALID],
            ["no",          I::VALID],
            ["true",        I::VALID],
            ["false",       I::VALID],
            [INF,           0],
            [-INF,          0],
            [NAN,           0],
            [[],            0],
            ["some string", I::VALID],
            ["           ", I::VALID | I::WHITE],
            [new \StdClass, 0],
            [new StrClass(""),    I::VALID | I::EMPTY],
            [new StrClass("1"),   I::VALID],
            [new StrClass("0"),   I::VALID],
            [new StrClass("-1"),  I::VALID],
            [new StrClass("Msg"), I::VALID],
            [new StrClass("   "), I::VALID | I::WHITE],
        ];
        foreach ($tests as $test) {
            list($value, $exp) = $test;
            $this->assertSame($exp, I::str($value), "Test returned ".decbin(I::str($value))." for value: ".var_export($value, true));
        }
    }

    public function testValidateDatabaseIdentifier() {
        $tests = [
            [null,          false, true],
            ["",            false, true],
            [1,             true,  true],
            [PHP_INT_MAX,   true,  true],
            [1.0,           true,  true],
            ["1.0",         true,  true],
            ["001.0",       true,  true],
            ["1.0e2",       true,  true],
            ["1",           true,  true],
            ["001",         true,  true],
            ["1e2",         true,  true],
            ["+1.0",        true,  true],
            ["+001.0",      true,  true],
            ["+1.0e2",      true,  true],
            ["+1",          true,  true],
            ["+001",        true,  true],
            ["+1e2",        true,  true],
            [0,             false, true],
            ["0",           false, true],
            ["000",         false, true],
            [0.0,           false, true],
            ["0.0",         false, true],
            ["000.000",     false, true],
            ["+0",          false, true],
            ["+000",        false, true],
            ["+0.0",        false, true],
            ["+000.000",    false, true],
            [-1,            false, false],
            [-1.0,          false, false],
            ["-1.0",        false, false],
            ["-001.0",      false, false],
            ["-1.0e2",      false, false],
            ["-1",          false, false],
            ["-001",        false, false],
            ["-1e2",        false, false],
            [-0,            false, true],
            ["-0",          false, true],
            ["-000",        false, true],
            [-0.0,          false, true],
            ["-0.0",        false, true],
            ["-000.000",    false, true],
            [false,         false, false],
            [true,          false, false],
            ["on",          false, false],
            ["off",         false, false],
            ["yes",         false, false],
            ["no",          false, false],
            ["true",        false, false],
            ["false",       false, false],
            [INF,           false, false],
            [-INF,          false, false],
            [NAN,           false, false],
            [[],            false, false],
            ["some string", false, false],
            ["           ", false, false],
            [new \StdClass, false, false],
            [new StrClass(""),    false, true],
            [new StrClass("1"),   true,  true],
            [new StrClass("0"),   false, true],
            [new StrClass("-1"),  false, false],
            [new StrClass("Msg"), false, false],
            [new StrClass("   "), false, false],
        ];
        foreach ($tests as $test) {
            list($value, $exp, $expNull) = $test;
            $this->assertSame($exp, I::id($value), "Non-null test failed for value: ".var_export($value, true));
            $this->assertSame($expNull, I::id($value, true), "Null test failed for value: ".var_export($value, true));
        }
    }

    public function testValidateBoolean() {
        $tests = [
            [null,          null],
            ["",            false],
            [1,             true],
            [PHP_INT_MAX,   null],
            [1.0,           true],
            ["1.0",         true],
            ["001.0",       true],
            ["1.0e2",       null],
            ["1",           true],
            ["001",         true],
            ["1e2",         null],
            ["+1.0",        true],
            ["+001.0",      true],
            ["+1.0e2",      null],
            ["+1",          true],
            ["+001",        true],
            ["+1e2",        null],
            [0,             false],
            ["0",           false],
            ["000",         false],
            [0.0,           false],
            ["0.0",         false],
            ["000.000",     false],
            ["+0",          false],
            ["+000",        false],
            ["+0.0",        false],
            ["+000.000",    false],
            [-1,            null],
            [-1.0,          null],
            ["-1.0",        null],
            ["-001.0",      null],
            ["-1.0e2",      null],
            ["-1",          null],
            ["-001",        null],
            ["-1e2",        null],
            [-0,            false],
            ["-0",          false],
            ["-000",        false],
            [-0.0,          false],
            ["-0.0",        false],
            ["-000.000",    false],
            [false,         false],
            [true,          true],
            ["on",          true],
            ["off",         false],
            ["yes",         true],
            ["no",          false],
            ["true",        true],
            ["false",       false],
            [INF,           null],
            [-INF,          null],
            [NAN,           null],
            [[],            null],
            ["some string", null],
            ["           ", null],
            [new \StdClass, null],
            [new StrClass(""),    false],
            [new StrClass("1"),   true],
            [new StrClass("0"),   false],
            [new StrClass("-1"),  null],
            [new StrClass("Msg"), null],
            [new StrClass("   "), null],
        ];
        foreach ($tests as $test) {
            list($value, $exp) = $test;
            $this->assertSame($exp, I::bool($value), "Null Test failed for value: ".var_export($value, true));
            if (is_null($exp)) {
                $this->assertTrue(I::bool($value, true), "True Test failed for value: ".var_export($value, true));
                $this->assertFalse(I::bool($value, false), "False Test failed for value: ".var_export($value, true));
            }
        }
    }

    public function testNormalizeValues() {
        $tests = [
            /* The test data are very dense for this set. Each value is normalized to each of the following types:

                - mixed (no normalization performed)
                - null
                - boolean
                - integer
                - float
                - string
                - array

               For each of these types, there is an expected output value, as well as a boolean indicating whether
               the value should pass or fail a strict normalization. Conversion to DateTime is covered below by a different data set
            */
            /* Input value                          null         bool           int                      float                        string                          array                                         */
            [null,                                  [null,true], [false,false], [0,              false], [0.0,                false], ["",                    false], [[],                                     false]],
            ["",                                    [null,true], [false,true],  [0,              false], [0.0,                false], ["",                    true],  [[""],                                   false]],
            [1,                                     [null,true], [true, true],  [1,              true],  [1.0,                true],  ["1",                   true],  [[1],                                    false]],
            [PHP_INT_MAX,                           [null,true], [true, false], [PHP_INT_MAX,    true],  [(float) PHP_INT_MAX,true],  [(string) PHP_INT_MAX,  true],  [[PHP_INT_MAX],                          false]],
            [1.0,                                   [null,true], [true, true],  [1,              true],  [1.0,                true],  ["1",                   true],  [[1.0],                                  false]],
            ["1.0",                                 [null,true], [true, true],  [1,              true],  [1.0,                true],  ["1.0",                 true],  [["1.0"],                                false]],
            ["001.0",                               [null,true], [true, true],  [1,              true],  [1.0,                true],  ["001.0",               true],  [["001.0"],                              false]],
            ["1.0e2",                               [null,true], [true, false], [100,            true],  [100.0,              true],  ["1.0e2",               true],  [["1.0e2"],                              false]],
            ["1",                                   [null,true], [true, true],  [1,              true],  [1.0,                true],  ["1",                   true],  [["1"],                                  false]],
            ["001",                                 [null,true], [true, true],  [1,              true],  [1.0,                true],  ["001",                 true],  [["001"],                                false]],
            ["1e2",                                 [null,true], [true, false], [100,            true],  [100.0,              true],  ["1e2",                 true],  [["1e2"],                                false]],
            ["+1.0",                                [null,true], [true, true],  [1,              true],  [1.0,                true],  ["+1.0",                true],  [["+1.0"],                               false]],
            ["+001.0",                              [null,true], [true, true],  [1,              true],  [1.0,                true],  ["+001.0",              true],  [["+001.0"],                             false]],
            ["+1.0e2",                              [null,true], [true, false], [100,            true],  [100.0,              true],  ["+1.0e2",              true],  [["+1.0e2"],                             false]],
            ["+1",                                  [null,true], [true, true],  [1,              true],  [1.0,                true],  ["+1",                  true],  [["+1"],                                 false]],
            ["+001",                                [null,true], [true, true],  [1,              true],  [1.0,                true],  ["+001",                true],  [["+001"],                               false]],
            ["+1e2",                                [null,true], [true, false], [100,            true],  [100.0,              true],  ["+1e2",                true],  [["+1e2"],                               false]],
            [0,                                     [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0",                   true],  [[0],                                    false]],
            ["0",                                   [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0",                   true],  [["0"],                                  false]],
            ["000",                                 [null,true], [false,true],  [0,              true],  [0.0,                true],  ["000",                 true],  [["000"],                                false]],
            [0.0,                                   [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0",                   true],  [[0.0],                                  false]],
            ["0.0",                                 [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0.0",                 true],  [["0.0"],                                false]],
            ["000.000",                             [null,true], [false,true],  [0,              true],  [0.0,                true],  ["000.000",             true],  [["000.000"],                            false]],
            ["+0",                                  [null,true], [false,true],  [0,              true],  [0.0,                true],  ["+0",                  true],  [["+0"],                                 false]],
            ["+000",                                [null,true], [false,true],  [0,              true],  [0.0,                true],  ["+000",                true],  [["+000"],                               false]],
            ["+0.0",                                [null,true], [false,true],  [0,              true],  [0.0,                true],  ["+0.0",                true],  [["+0.0"],                               false]],
            ["+000.000",                            [null,true], [false,true],  [0,              true],  [0.0,                true],  ["+000.000",            true],  [["+000.000"],                           false]],
            [-1,                                    [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-1",                  true],  [[-1],                                   false]],
            [-1.0,                                  [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-1",                  true],  [[-1.0],                                 false]],
            ["-1.0",                                [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-1.0",                true],  [["-1.0"],                               false]],
            ["-001.0",                              [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-001.0",              true],  [["-001.0"],                             false]],
            ["-1.0e2",                              [null,true], [true, false], [-100,           true],  [-100.0,             true],  ["-1.0e2",              true],  [["-1.0e2"],                             false]],
            ["-1",                                  [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-1",                  true],  [["-1"],                                 false]],
            ["-001",                                [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-001",                true],  [["-001"],                               false]],
            ["-1e2",                                [null,true], [true, false], [-100,           true],  [-100.0,             true],  ["-1e2",                true],  [["-1e2"],                               false]],
            [-0,                                    [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0",                   true],  [[-0],                                   false]],
            ["-0",                                  [null,true], [false,true],  [0,              true],  [-0.0,               true],  ["-0",                  true],  [["-0"],                                 false]],
            ["-000",                                [null,true], [false,true],  [0,              true],  [-0.0,               true],  ["-000",                true],  [["-000"],                               false]],
            [-0.0,                                  [null,true], [false,true],  [0,              true],  [-0.0,               true],  ["-0",                  true],  [[-0.0],                                 false]],
            ["-0.0",                                [null,true], [false,true],  [0,              true],  [-0.0,               true],  ["-0.0",                true],  [["-0.0"],                               false]],
            ["-000.000",                            [null,true], [false,true],  [0,              true],  [-0.0,               true],  ["-000.000",            true],  [["-000.000"],                           false]],
            [false,                                 [null,true], [false,true],  [0,              false], [0.0,                false], ["",                    false], [[false],                                false]],
            [true,                                  [null,true], [true, true],  [1,              false], [1.0,                false], ["1",                   false], [[true],                                 false]],
            ["on",                                  [null,true], [true, true],  [0,              false], [0.0,                false], ["on",                  true],  [["on"],                                 false]],
            ["off",                                 [null,true], [false,true],  [0,              false], [0.0,                false], ["off",                 true],  [["off"],                                false]],
            ["yes",                                 [null,true], [true, true],  [0,              false], [0.0,                false], ["yes",                 true],  [["yes"],                                false]],
            ["no",                                  [null,true], [false,true],  [0,              false], [0.0,                false], ["no",                  true],  [["no"],                                 false]],
            ["true",                                [null,true], [true, true],  [0,              false], [0.0,                false], ["true",                true],  [["true"],                               false]],
            ["false",                               [null,true], [false,true],  [0,              false], [0.0,                false], ["false",               true],  [["false"],                              false]],
            [INF,                                   [null,true], [true, false], [0,              false], [INF,                true],  ["INF",                 false], [[INF],                                  false]],
            [-INF,                                  [null,true], [true, false], [0,              false], [-INF,               true],  ["-INF",                false], [[-INF],                                 false]],
            [NAN,                                   [null,true], [false,false], [0,              false], [NAN,                true],  ["NAN",                 false], [[],                                     false]],
            [[],                                    [null,true], [false,false], [0,              false], [0.0,                false], ["",                    false], [[],                                     true] ],
            ["some string",                         [null,true], [true, false], [0,              false], [0.0,                false], ["some string",         true],  [["some string"],                        false]],
            ["           ",                         [null,true], [true, false], [0,              false], [0.0,                false], ["           ",         true],  [["           "],                        false]],
            [new \StdClass,                         [null,true], [true, false], [0,              false], [0.0,                false], ["",                    false], [[new \StdClass],                        false]],
            [new StrClass(""),                      [null,true], [false,true],  [0,              false], [0.0,                false], ["",                    true],  [[new StrClass("")],                     false]],
            [new StrClass("1"),                     [null,true], [true, true],  [1,              true],  [1.0,                true],  ["1",                   true],  [[new StrClass("1")],                    false]],
            [new StrClass("0"),                     [null,true], [false,true],  [0,              true],  [0.0,                true],  ["0",                   true],  [[new StrClass("0")],                    false]],
            [new StrClass("-1"),                    [null,true], [true, false], [-1,             true],  [-1.0,               true],  ["-1",                  true],  [[new StrClass("-1")],                   false]],
            [new StrClass("Msg"),                   [null,true], [true, false], [0,              false], [0.0,                false], ["Msg",                 true],  [[new StrClass("Msg")],                  false]],
            [new StrClass("   "),                   [null,true], [true, false], [0,              false], [0.0,                false], ["   ",                 true],  [[new StrClass("   ")],                  false]],
            [2.5,                                   [null,true], [true, false], [2,              false], [2.5,                true],  ["2.5",                 true],  [[2.5],                                  false]],
            [0.5,                                   [null,true], [true, false], [0,              false], [0.5,                true],  ["0.5",                 true],  [[0.5],                                  false]],
            ["2.5",                                 [null,true], [true, false], [2,              false], [2.5,                true],  ["2.5",                 true],  [["2.5"],                                false]],
            ["0.5",                                 [null,true], [true, false], [0,              false], [0.5,                true],  ["0.5",                 true],  [["0.5"],                                false]],
            [$this->d("2010-01-01T00:00:00", 0, 0), [null,true], [true, false], [1262304000,     false], [1262304000.0,       false], ["2010-01-01T00:00:00Z",true],  [[$this->d("2010-01-01T00:00:00", 0, 0)],false]],
            [$this->d("2010-01-01T00:00:00", 0, 1), [null,true], [true, false], [1262304000,     false], [1262304000.0,       false], ["2010-01-01T00:00:00Z",true],  [[$this->d("2010-01-01T00:00:00", 0, 1)],false]],
            [$this->d("2010-01-01T00:00:00", 1, 0), [null,true], [true, false], [1262322000,     false], [1262322000.0,       false], ["2010-01-01T05:00:00Z",true],  [[$this->d("2010-01-01T00:00:00", 1, 0)],false]],
            [$this->d("2010-01-01T00:00:00", 1, 1), [null,true], [true, false], [1262322000,     false], [1262322000.0,       false], ["2010-01-01T05:00:00Z",true],  [[$this->d("2010-01-01T00:00:00", 1, 1)],false]],
            [1e14,                                  [null,true], [true, false], [100000000000000,true],  [1e14,               true],  ["100000000000000",     true],  [[1e14],                                 false]],
            [1e-6,                                  [null,true], [true, false], [0,              false], [1e-6,               true],  ["0.000001",            true],  [[1e-6],                                 false]],
            [[1,2,3],                               [null,true], [true, false], [0,              false], [0.0,                false], ["",                    false], [[1,2,3],                                true] ],
            [['a'=>1,'b'=>2],                       [null,true], [true, false], [0,              false], [0.0,                false], ["",                    false], [['a'=>1,'b'=>2],                        true] ],
            [new Result([['a'=>1,'b'=>2]]),         [null,true], [true, false], [0,              false], [0.0,                false], ["",                    false], [[['a'=>1,'b'=>2]],                      true] ],
        ];
        $params = [
            [I::T_MIXED,  "Mixed"         ],
            [I::T_NULL,   "Null",         ],
            [I::T_BOOL,   "Boolean",      ],
            [I::T_INT,    "Integer",      ],
            [I::T_FLOAT,  "Floating point"],
            [I::T_STRING, "String",       ],
            [I::T_ARRAY,  "Array",        ],
        ];
        $assert = function($exp, $act, string $msg) {
            if (is_null($exp)) {
                $this->assertNull($act, $msg);
            } elseif (is_float($exp) && is_nan($exp)) {
                $this->assertNan($act, $msg);
            } elseif (is_scalar($exp)) {
                $this->assertSame($exp, $act, $msg);
            } else {
                $this->assertEquals($exp, $act, $msg);
            }
        };
        foreach ($params as $index => $param) {
            list($type, $name) = $param;
            $assert(null, I::normalize(null, $type | I::M_STRICT | I::M_NULL), $name." null-passthrough test failed");
            foreach ($tests as $test) {
                list($exp, $pass) = $index ? $test[$index] : [$test[$index], true];
                $value = $test[0];
                $assert($exp, I::normalize($value, $type), $name." test failed for value: ".var_export($value, true));
                if ($pass) {
                    $assert($exp, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true));
                    $assert($exp, I::normalize($value, $type | I::M_STRICT), $name." error test failed for value: ".var_export($value, true));
                } else {
                    $assert(null, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true));
                    $exc = new ExceptionType("strictFailure", $type);
                    try {
                        $act = I::normalize($value, $type | I::M_STRICT);
                    } catch (ExceptionType $e) {
                        $act = $e;
                    } finally {
                        $assert($exc, $act, $name." error test failed for value: ".var_export($value, true));
                    }
                }
            }
        }
        // DateTimeInterface tests
        $tests = [
            /* Input value                          microtime                    iso8601                      iso8601m                     http                         sql                          date                         time                         unix                         float                        '!M j, Y (D)'                *strtotime* (null)                  */
            [null,                                  null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                                   ],
            [$this->d("2010-01-01T00:00:00", 0, 0), $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),                   ],
            [$this->d("2010-01-01T00:00:00", 0, 1), $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),                   ],
            [$this->d("2010-01-01T00:00:00", 1, 0), $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),                   ],
            [$this->d("2010-01-01T00:00:00", 1, 1), $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),        $this->t(1262322000),                   ],
            [1262304000,                            $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),        $this->t(1262304000),                   ],
            [1262304000.123456,                     $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456), $this->t(1262304000.123456),            ],
            [1262304000.42,                         $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),     $this->t(1262304000.42),                ],
            ["0.12345600 1262304000",               $this->t(1262304000.123456), null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                                   ],
            ["0.42 1262304000",                     null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                                   ],
            ["2010-01-01T00:00:00",                 null,                        $this->t(1262304000),        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01T00:00:00Z",                null,                        $this->t(1262304000),        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01T00:00:00+0000",            null,                        $this->t(1262304000),        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01T00:00:00-0000",            null,                        $this->t(1262304000),        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01T00:00:00+00:00",           null,                        $this->t(1262304000),        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01T00:00:00-05:00",           null,                        $this->t(1262322000),        $this->t(1262322000),        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262322000),                   ],
            ["2010-01-01T00:00:00.123456Z",         null,                        null,                        $this->t(1262304000.123456), null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000.123456),            ],
            ["Fri, 01 Jan 2010 00:00:00 GMT",       null,                        null,                        null,                        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01 00:00:00",                 null,                        null,                        null,                        null,                        $this->t(1262304000),        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["2010-01-01",                          null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
            ["12:34:56",                            null,                        null,                        null,                        null,                        null,                        null,                        $this->t(45296),             null,                        null,                        null,                        $this->t(date_create("today", new \DateTimezone("UTC"))->getTimestamp()+45296), ],
            ["1262304000",                          null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),        null,                        null,                        null,                                   ],
            ["1262304000.123456",                   null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000.123456), null,                        null,                                   ],
            ["1262304000.42",                       null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000.42),     null,                        null,                                   ],
            ["Jan 1, 2010 (Fri)",                   null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),        null,                                   ],
            ["First day of Jan 2010 12AM",          null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        null,                        $this->t(1262304000),                   ],
        ];
        $formats = [
            "microtime",
            "iso8601",
            "iso8601m",
            "http",
            "sql",
            "date",
            "time",
            "unix",
            "float",
            "!M j, Y (D)",
            null,
        ];
        $exc = new ExceptionType("strictFailure", I::T_DATE);
        foreach ($formats as $index => $format) {
            foreach ($tests as $test) {
                $value = $test[0];
                $exp = $test[$index+1];
                $this->assertEquals($exp, I::normalize($value, I::T_DATE, $format), "Test failed for format ".var_export($format, true)." using value ".var_export($value, true));
                $this->assertEquals($exp, I::normalize($value, I::T_DATE | I::M_DROP, $format), "Drop test failed for format ".var_export($format, true)." using value ".var_export($value, true));
                // test for exception in case of errors
                $exp = $exp ?? $exc;
                try {
                    $act = I::normalize($value, I::T_DATE | I::M_STRICT, $format);
                } catch (ExceptionType $e) {
                    $act = $e;
                } finally {
                    $this->assertEquals($exp, $act, "Error test failed for format ".var_export($format, true)." using value ".var_export($value, true));
                }
            }
        }
        // Array-mode tests
        $tests = [
            [I::T_INT    | I::M_DROP,   new Result([1, 2, 2.2, 3]), [1,2,null,3]   ],
            [I::T_INT,                  new Result([1, 2, 2.2, 3]), [1,2,2,3]      ],
            [I::T_STRING | I::M_STRICT, "Bare string",                   ["Bare string"]],
        ];
        foreach ($tests as $index => $test) {
            list($type, $value, $exp) = $test;
            $this->assertEquals($exp, I::normalize($value, $type | I::M_ARRAY, "iso8601"), "Failed test #$index");
        }
        // Date-to-string format tests
        $test = new \DateTimeImmutable("now", new \DateTimezone("UTC"));
        $exp = $test->format(I::DATE_FORMATS['iso8601'][1]);
        $this->assertSame($exp, I::normalize($test, I::T_STRING, null), "Failed test for null output date format");
        foreach (I::DATE_FORMATS as $name => $formats) {
            $exp = $test->format($formats[1]);
            $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $name), "Failed test for output date format '$name'");
        }
        foreach (["U", "M j, Y (D)", "r", "c"] as $format) {
            $exp = $test->format($format);
            $this->assertSame($exp, I::normalize($test, I::T_STRING, null, $format), "Failed test for output date format '$format'");
        }
    }

    protected function d($spec, $local, $immutable): \DateTimeInterface {
        $tz = $local ? new \DateTimeZone("America/Toronto") : new \DateTimeZone("UTC");
        if ($immutable) {
            return \DateTimeImmutable::createFromFormat("!Y-m-d\TH:i:s", $spec, $tz);
        } else {
            return \DateTime::createFromFormat("!Y-m-d\TH:i:s", $spec, $tz);
        }
    }

    protected function t(float $spec): \DateTimeImmutable {
        return \DateTimeImmutable::createFromFormat("U.u", sprintf("%F", $spec), new \DateTimeZone("UTC"));
    }
}