<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop\Unit\DTO;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop\DTO\ParameterBagInterface;

/**
 * Tests the ParameterBag DTO.
 *
 * @coversDefaultClass \Drupal\flowdrop\DTO\ParameterBag
 * @group flowdrop
 */
class ParameterBagTest extends UnitTestCase {

  /**
   * Tests that ParameterBag implements ParameterBagInterface.
   */
  public function testImplementsInterface(): void {
    $bag = new ParameterBag();
    $this->assertInstanceOf(ParameterBagInterface::class, $bag);
    $this->assertInstanceOf(\JsonSerializable::class, $bag);
  }

  /**
   * @covers ::get
   */
  public function testGet(): void {
    $bag = new ParameterBag(["key" => "value"]);
    $this->assertEquals("value", $bag->get("key"));
    $this->assertNull($bag->get("missing"));
    $this->assertEquals("default", $bag->get("missing", "default"));
  }

  /**
   * @covers ::has
   */
  public function testHas(): void {
    $bag = new ParameterBag(["exists" => "value", "null_val" => NULL]);
    $this->assertTrue($bag->has("exists"));
    $this->assertTrue($bag->has("null_val"));
    $this->assertFalse($bag->has("missing"));
  }

  /**
   * @covers ::all
   */
  public function testAll(): void {
    $params = ["a" => 1, "b" => "two"];
    $bag = new ParameterBag($params);
    $this->assertEquals($params, $bag->all());
  }

  /**
   * @covers ::keys
   */
  public function testKeys(): void {
    $bag = new ParameterBag(["x" => 1, "y" => 2]);
    $this->assertEquals(["x", "y"], $bag->keys());
  }

  /**
   * @covers ::isEmpty
   */
  public function testIsEmpty(): void {
    $this->assertTrue((new ParameterBag())->isEmpty());
    $this->assertFalse((new ParameterBag(["a" => 1]))->isEmpty());
  }

  /**
   * @covers ::getString
   */
  public function testGetString(): void {
    $bag = new ParameterBag(["str" => "hello", "num" => 42, "arr" => [1, 2]]);
    $this->assertEquals("hello", $bag->getString("str"));
    $this->assertEquals("42", $bag->getString("num"));
    $this->assertEquals("", $bag->getString("missing"));
    $this->assertEquals("fallback", $bag->getString("missing", "fallback"));
    $this->assertEquals("", $bag->getString("arr"));
  }

  /**
   * @covers ::getInt
   */
  public function testGetInt(): void {
    $bag = new ParameterBag(["int" => 42, "str" => "10", "bad" => "abc"]);
    $this->assertEquals(42, $bag->getInt("int"));
    $this->assertEquals(10, $bag->getInt("str"));
    $this->assertEquals(0, $bag->getInt("bad"));
    $this->assertEquals(0, $bag->getInt("missing"));
    $this->assertEquals(99, $bag->getInt("missing", 99));
  }

  /**
   * @covers ::getFloat
   */
  public function testGetFloat(): void {
    $bag = new ParameterBag(["float" => 3.14, "int" => 5, "str" => "2.5", "bad" => "abc"]);
    $this->assertEquals(3.14, $bag->getFloat("float"));
    $this->assertEquals(5.0, $bag->getFloat("int"));
    $this->assertEquals(2.5, $bag->getFloat("str"));
    $this->assertEquals(0.0, $bag->getFloat("bad"));
    $this->assertEquals(0.0, $bag->getFloat("missing"));
  }

  /**
   * @covers ::getBool
   */
  public function testGetBool(): void {
    $bag = new ParameterBag([
      "true" => TRUE,
      "false" => FALSE,
      "one" => 1,
      "zero" => 0,
      "str_true" => "true",
      "str_false" => "false",
      "str_1" => "1",
      "str_0" => "0",
      "empty" => "",
    ]);
    $this->assertTrue($bag->getBool("true"));
    $this->assertFalse($bag->getBool("false"));
    $this->assertTrue($bag->getBool("one"));
    $this->assertFalse($bag->getBool("zero"));
    $this->assertTrue($bag->getBool("str_true"));
    $this->assertFalse($bag->getBool("str_false"));
    $this->assertTrue($bag->getBool("str_1"));
    $this->assertFalse($bag->getBool("str_0"));
    $this->assertFalse($bag->getBool("empty"));
    $this->assertFalse($bag->getBool("missing"));
  }

  /**
   * @covers ::getArray
   */
  public function testGetArray(): void {
    $bag = new ParameterBag(["arr" => [1, 2, 3], "str" => "not_array"]);
    $this->assertEquals([1, 2, 3], $bag->getArray("arr"));
    $this->assertEquals([], $bag->getArray("str"));
    $this->assertEquals([], $bag->getArray("missing"));
    $this->assertEquals(["default"], $bag->getArray("missing", ["default"]));
  }

  /**
   * @covers ::getTyped
   */
  public function testGetTyped(): void {
    $bag = new ParameterBag(["str" => "hello", "int" => 42]);
    $this->assertEquals("hello", $bag->getTyped("str", "string"));
    $this->assertEquals(42, $bag->getTyped("int", "int"));
    $this->assertNull($bag->getTyped("missing", "string"));
  }

  /**
   * @covers ::getTyped
   */
  public function testGetTypedThrowsOnMismatch(): void {
    $bag = new ParameterBag(["str" => "hello"]);
    $this->expectException(\InvalidArgumentException::class);
    $bag->getTyped("str", "int");
  }

  /**
   * @covers ::getTyped
   */
  public function testGetTypedIntToFloat(): void {
    $bag = new ParameterBag(["int" => 5]);
    $this->assertEquals(5.0, $bag->getTyped("int", "float"));
  }

  /**
   * @covers ::fromArray
   */
  public function testFromArray(): void {
    $bag = ParameterBag::fromArray(["a" => 1, "b" => 2]);
    $this->assertEquals(1, $bag->get("a"));
    $this->assertEquals(2, $bag->get("b"));
  }

  /**
   * @covers ::empty
   */
  public function testEmpty(): void {
    $bag = ParameterBag::empty();
    $this->assertTrue($bag->isEmpty());
  }

  /**
   * @covers ::with
   */
  public function testWith(): void {
    $original = new ParameterBag(["a" => 1]);
    $new = $original->with(["b" => 2]);
    $this->assertNotSame($original, $new);
    $this->assertFalse($original->has("b"));
    $this->assertEquals(1, $new->get("a"));
    $this->assertEquals(2, $new->get("b"));
  }

  /**
   * @covers ::without
   */
  public function testWithout(): void {
    $original = new ParameterBag(["a" => 1, "b" => 2, "c" => 3]);
    $new = $original->without(["b"]);
    $this->assertNotSame($original, $new);
    $this->assertTrue($original->has("b"));
    $this->assertFalse($new->has("b"));
    $this->assertEquals(1, $new->get("a"));
    $this->assertEquals(3, $new->get("c"));
  }

  /**
   * @covers ::jsonSerialize
   */
  public function testJsonSerialize(): void {
    $params = ["key" => "value", "num" => 42];
    $bag = new ParameterBag($params);
    $json = json_encode($bag);
    $this->assertIsString($json);
    $decoded = json_decode($json, TRUE);
    $this->assertEquals($params, $decoded);
  }

}
