<?php
namespace nur\data\types;

use nur\b\ValueException;
use nur\data\types\impl\FirstType;
use nur\data\types\impl\Point;
use nur\data\types\impl\SecondType;
use nur\data\types\impl\ThirdType;
use nur\ref\ref_type;
use nur\t\TestCase;

class PhpIncarnationTest extends TestCase {
  const DEFAULT_TYPES = [
    ref_type::MIXED,
    ref_type::BOOL, "bool",
    ref_type::INT, "int",
    ref_type::FLOAT, "flt", "double", "dbl",
    ref_type::RAWSTRING, "rawstr",
    ref_type::STRING, "str", "text", "txt",
    ref_type::KEY,
    ref_type::DATETIME,
    ref_type::DATE,
    ref_type::TIME,
    ref_type::HOUR,
    ref_type::NMIXED,
    "?". ref_type::BOOL, "?bool",
    ref_type::TRIBOOL, "trib",
    "?". ref_type::TRIBOOL, "?trib",
    "?". ref_type::INT, "?int",
    "?". ref_type::FLOAT, "?flt", "?double", "?dbl",
    "?". ref_type::RAWSTRING, "?rawstr",
    "?". ref_type::STRING, "?str", "?text", "?txt",
    "?". ref_type::KEY,
    "?". ref_type::DATETIME,
    "?". ref_type::DATE,
    "?". ref_type::TIME,
    "?". ref_type::HOUR,
    "?array",
    "array",
    "?array[]",
    "array[]",
    "?iterable",
    "iterable",
    "?resource",
    "resource",
  ];

  function testHasType() {
    $k = new PhpIncarnation();
    foreach (self::DEFAULT_TYPES as $name) {
      self::assertTrue($k->hasType($name), $name);
    }
    # string
    $k->addType(FirstType::class);
    self::assertTrue($k->hasType(FirstType::class), FirstType::class);
    $k->addType(FirstType::class, "premier");
    self::assertTrue($k->hasType("premier"), "premier");
    # array, anonyme
    $k->addType([SecondType::class, "args"]);
    self::assertFalse($k->hasType(SecondType::class), SecondType::class);
    self::assertTrue($k->hasType([SecondType::class, "args"]), SecondType::class);
    $k->addType([SecondType::class, "args"], "deuxieme");
    self::assertTrue($k->hasType("deuxieme"), "deuxieme");
    # array, nommé
    $k->addType(["third" => ThirdType::class, "args"]);
    self::assertTrue($k->hasType("third"), "third");
    $k->addType(["third" => ThirdType::class, "args"], "troisieme");
    self::assertTrue($k->hasType("troisieme"), "troisieme");
  }

  function testGetType() {
    $k = new PhpIncarnation();

    $stringType = $k->getType("string");
    self::assertInstanceOf(StringType::class, $stringType);
    self::assertFalse($stringType->isAllowNull());
    $stringType = $k->getType("?string");
    self::assertInstanceOf(StringType::class, $stringType);
    self::assertTrue($stringType->isAllowNull());
    
    $firstType = $k->getType(FirstType::class);
    self::assertInstanceOf(FirstType::class, $firstType);
    $point = $firstType->with(1);
    self::assertPoint(new Point(1, Point::Y), $point);
    
    $secondType = $k->getType([SecondType::class]);
    self::assertInstanceOf(SecondType::class, $secondType);
    $point = $secondType->with(2);
    self::assertPoint(new Point(2, Point::Y), $point);
    $secondType2 = $k->getType([SecondType::class]);
    self::assertSame($secondType, $secondType2);

    $secondType3 = $k->getType([SecondType::class, ["name" => "whatever"]]);
    self::assertInstanceOf(SecondType::class, $secondType3);
    $point = $secondType3->with(2);
    self::assertPoint(new Point(2, Point::Y), $point);
    $secondType4 = $k->getType([SecondType::class, ["name" => "whatever"]]);
    self::assertSame($secondType3, $secondType4);
    self::assertNotSame($secondType3, $secondType);

    $thirdType = $k->getType(["third" => ThirdType::class]);
    self::assertInstanceOf(ThirdType::class, $thirdType);
    $point = $thirdType->with(3);
    self::assertPoint(new Point(3, Point::Y), $point);

    $thirdType = $k->getType("third");
    self::assertInstanceOf(ThirdType::class, $thirdType);
    $point = $thirdType->with(4);
    self::assertPoint(new Point(4, Point::Y), $point);
  }

  function testAddType() {
    $k = new PhpIncarnation();

    $k->addType(Point::class, "point");
    $point = $k->getType("point")->with(1);
    self::assertPoint(new Point(1, Point::Y), $point);

    $k->addType([FirstType::class]);
    $type1 = $k->getType([FirstType::class]);
    $k->addType([FirstType::class]);
    $type2 = $k->getType([FirstType::class]);
    self::assertSame($type2, $type1);
  }

  const BASIC_SCHEMA = [
    "any" => null,
    "mixed" => "mixed",
    "bool" => "bool",
    "nbool" => "?bool",
    "int" => "int",
    "nint" => "?int",
    "float" => "float",
    "nfloat" => "?float",
    "string" => "string",
    "nstring" => "?string",
    "array" => "array",
    "narray" => "?array",
    "arrays" => "array[]",
    "narrays" => "?array[]",
    "iterable" => "iterable",
    "niterable" => "?iterable",
  ];

  function testBasic() {
    $md = new Metadata(self::BASIC_SCHEMA);

    $data = null;
    $md->ensureSchema($data);
    self::assertSame([
      "any" => null,
      "mixed" => null,
      "bool" => false, "nbool" => null,
      "int" => 0, "nint" => null,
      "float" => 0.0, "nfloat" => null,
      "string" => "", "nstring" => null,
      "array" => [], "narray" => null,
      "arrays" => [], "narrays" => null,
      "iterable" => [], "niterable" => null,
    ], $data);
    
    $data = [];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => null,
      "mixed" => null,
      "bool" => false, "nbool" => null,
      "int" => 0, "nint" => null,
      "float" => 0.0, "nfloat" => null,
      "string" => "", "nstring" => null,
      "array" => [], "narray" => null,
      "arrays" => [], "narrays" => null,
      "iterable" => [], "niterable" => null,
    ], $data);

    $data = [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => null,
      "mixed" => null,
      "bool" => false, "nbool" => null,
      "int" => 0, "nint" => null,
      "float" => 0.0, "nfloat" => null,
      "string" => "", "nstring" => null,
      "array" => [], "narray" => null,
      "arrays" => [], "narrays" => null,
      "iterable" => [], "niterable" => null,
    ], $data);
    
    $data = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => null,
      "mixed" => false,
      "bool" => false, "nbool" => false,
      "int" => 0, "nint" => null,
      "float" => 0.0, "nfloat" => null,
      "string" => "", "nstring" => null,
      "array" => [], "narray" => null,
      "arrays" => [], "narrays" => null,
      "iterable" => [], "niterable" => null,
    ], $data);
    
    $data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => 0,
      "mixed" => 0,
      "bool" => false, "nbool" => false,
      "int" => 0, "nint" => 0,
      "float" => 0.0, "nfloat" => 0.0,
      "string" => "0", "nstring" => "0",
      "array" => [0], "narray" => [0],
      "arrays" => [[0]], "narrays" => [[0]],
      "iterable" => [0], "niterable" => [0],
    ], $data);
    
    $data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => 1,
      "mixed" => 1,
      "bool" => true, "nbool" => true,
      "int" => 1, "nint" => 1,
      "float" => 1.0, "nfloat" => 1.0,
      "string" => "1", "nstring" => "1",
      "array" => [1], "narray" => [1],
      "arrays" => [[1]], "narrays" => [[1]],
      "iterable" => [1], "niterable" => [1],
    ], $data);
    
    $data = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => "",
      "mixed" => "",
      "bool" => false, "nbool" => false,
      "int" => 0, "nint" => 0,
      "float" => 0.0, "nfloat" => 0.0,
      "string" => "", "nstring" => "",
      "array" => [""], "narray" => [""],
      "arrays" => [[""]], "narrays" => [[""]],
      "iterable" => [""], "niterable" => [""],
    ], $data);
    
    $data = ["a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"];
    $md->ensureSchema($data);
    self::assertSame([
      "any" => "a",
      "mixed" => "a",
      "bool" => true, "nbool" => true,
      "int" => 0, "nint" => 0,
      "float" => 0.0, "nfloat" => 0.0,
      "string" => "a", "nstring" => "a",
      "array" => ["a"], "narray" => ["a"],
      "arrays" => [["a"]], "narrays" => [["a"]],
      "iterable" => ["a"], "niterable" => ["a"],
    ], $data);
  }
  
  const SPECIAL_SCHEMA = [
    "resource" => "?resource",
  ];

  function testSpecial() {
    $md = new Metadata(self::SPECIAL_SCHEMA);

    $data = null;
    $md->ensureSchema($data);
    self::assertSame(["resource" => null], $data);

    $data = [];
    $md->ensureSchema($data);
    self::assertSame(["resource" => null], $data);

    $data = [null];
    $md->ensureSchema($data);
    self::assertSame(["resource" => null], $data);

    $data = [false];
    $md->ensureSchema($data);
    self::assertSame(["resource" => null], $data);

    $data = ["x"];
    self::assertException(ValueException::class, function() use ($md, $data) {
      $md->ensureSchema($data);
    });

    $inf = fopen("php://memory", "rb");
    $data = [$inf];
    $md->ensureSchema($data);
    self::assertSame(["resource" => $inf], $data);
    fclose($inf);
  }

  static function assertPoint(?Point $a, ?Point $b) {
    if ($a === null) self::assertNull($b);
    if ($b === null) self::assertNull($a);
    if ($a !== null && $b !== null) {
      self::assertSame($a->x, $b->x);
      self::assertSame($a->y, $b->y);
    }
  }

  function testGenericVerifix() {
    $k = new PhpIncarnation();

    $value = null; $k->getType(Point::class)->verifix($value);
    self::assertPoint(null, $value);

    $value = 1; $k->getType(Point::class)->verifix($value);
    self::assertPoint(new Point(1, Point::Y), $value);

    $value = [1]; $k->getType(Point::class)->verifix($value);
    self::assertPoint(new Point(1, Point::Y), $value);

    $value = [1, 2]; $k->getType(Point::class)->verifix($value);
    self::assertPoint(new Point(1, 2), $value);
  }

  function testDynamic() {
    $k = new PhpIncarnation();

    $typedef = [RawStringType::class, ["allowed_values" => ["first", "second"]]];
    $t1 = $k->getType($typedef);
    self::assertException(ValueException::class, [$t1, "with"], "  first  ");
    self::assertException(ValueException::class, [$t1, "with"], "  second  ");
    self::assertException(ValueException::class, [$t1, "with"], "  third  ");

    $typedef = [StringType::class, ["allowed_values" => ["first", "second"]]];
    $t1 = $k->getType($typedef);
    self::assertSame("first", $t1->with("  first  "));
    self::assertSame("second", $t1->with("  second  "));
    self::assertException(ValueException::class, [$t1, "with"], "  third  ");

    $t2 = $k->getType($typedef);
    self::assertSame($t2, $t1);
  }

  function testClassAlias() {
    $k = new PhpIncarnation();

    $t1 = $k->getType("string"); // static
    $t2 = $k->getType(["string"]); // dynamique
    $t3 = $k->getType(["string"]); // dynamique
    self::assertNotSame($t2, $t1);
    self::assertSame($t3, $t2);

    $typedef = ["rawstring", ["allowed_values" => ["first", "second"]]];
    $t1 = $k->getType($typedef);
    self::assertException(ValueException::class, [$t1, "with"], "  first  ");
    self::assertException(ValueException::class, [$t1, "with"], "  second  ");
    self::assertException(ValueException::class, [$t1, "with"], "  third  ");

    $typedef = ["string", ["allowed_values" => ["first", "second"]]];
    $t1 = $k->getType($typedef);
    self::assertSame("first", $t1->with("  first  "));
    self::assertSame("second", $t1->with("  second  "));
    self::assertException(ValueException::class, [$t1, "with"], "  third  ");

    $t2 = $k->getType($typedef);
    self::assertSame($t2, $t1);
  }
}
