<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Unit\Reducer;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_stategraph\Reducer\MergeReducer;

/**
 * Tests the MergeReducer.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\Reducer\MergeReducer
 * @group flowdrop_stategraph
 */
class MergeReducerTest extends UnitTestCase {

  /**
   * The reducer under test.
   *
   * @var \Drupal\flowdrop_stategraph\Reducer\MergeReducer
   */
  private MergeReducer $reducer;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->reducer = new MergeReducer();
  }

  /**
   * Tests getId returns "merge".
   *
   * @covers ::getId
   */
  public function testGetId(): void {
    $this->assertEquals("merge", $this->reducer->getId());
  }

  /**
   * Tests getDefaultValue returns empty array.
   *
   * @covers ::getDefaultValue
   */
  public function testGetDefaultValue(): void {
    $this->assertEquals([], $this->reducer->getDefaultValue());
  }

  /**
   * Tests simple array merge.
   *
   * @covers ::reduce
   */
  public function testSimpleArrayMerge(): void {
    $current = ["key1" => "value1"];
    $update = ["key2" => "value2"];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals([
      "key1" => "value1",
      "key2" => "value2",
    ], $result);
  }

  /**
   * Tests deep nested array merge.
   *
   * @covers ::reduce
   */
  public function testDeepNestedMerge(): void {
    $current = [
      "user" => [
        "name" => "John",
        "age" => 30,
      ],
    ];

    $update = [
      "user" => [
        "email" => "john@example.com",
      ],
    ];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals([
      "user" => [
        "name" => "John",
        "age" => 30,
        "email" => "john@example.com",
      ],
    ], $result);
  }

  /**
   * Tests scalar value replacement within merge.
   *
   * @covers ::reduce
   */
  public function testScalarValueReplacement(): void {
    $current = ["key" => "old_value"];
    $update = ["key" => "new_value"];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals(["key" => "new_value"], $result);
  }

  /**
   * Tests merge preserves current when update is null.
   *
   * @covers ::reduce
   */
  public function testMergeWithNullUpdatePreservesCurrent(): void {
    $current = ["key" => "value"];

    $result = $this->reducer->reduce($current, NULL);

    $this->assertEquals(["key" => "value"], $result);
  }

  /**
   * Tests merge replaces when current is not array.
   *
   * @covers ::reduce
   */
  public function testMergeReplacesWhenCurrentNotArray(): void {
    $current = "not_an_array";
    $update = ["new" => "data"];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals(["new" => "data"], $result);
  }

  /**
   * Tests merge replaces when update is not array.
   *
   * @covers ::reduce
   */
  public function testMergeReplacesWhenUpdateNotArray(): void {
    $current = ["old" => "data"];
    $update = "not_an_array";

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals("not_an_array", $result);
  }

  /**
   * Tests numeric keys are appended, not merged by index.
   *
   * @covers ::reduce
   */
  public function testNumericKeysAppended(): void {
    $current = [0 => "a", 1 => "b"];
    $update = [0 => "c", 1 => "d"];

    $result = $this->reducer->reduce($current, $update);

    // Numeric keys should append, not replace by index.
    $this->assertEquals(["a", "b", "c", "d"], $result);
  }

  /**
   * Tests mixed numeric and string keys.
   *
   * @covers ::reduce
   */
  public function testMixedNumericAndStringKeys(): void {
    $current = [
      "name" => "John",
      0 => "first",
    ];

    $update = [
      "age" => 30,
      0 => "second",
    ];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals("John", $result["name"]);
    $this->assertEquals(30, $result["age"]);
    // Numeric keys appended.
    $this->assertContains("first", $result);
    $this->assertContains("second", $result);
  }

  /**
   * Tests three-level deep merge.
   *
   * @covers ::reduce
   */
  public function testThreeLevelDeepMerge(): void {
    $current = [
      "level1" => [
        "level2" => [
          "level3" => ["original" => "value"],
        ],
      ],
    ];

    $update = [
      "level1" => [
        "level2" => [
          "level3" => ["added" => "new"],
        ],
      ],
    ];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals([
      "level1" => [
        "level2" => [
          "level3" => [
            "original" => "value",
            "added" => "new",
          ],
        ],
      ],
    ], $result);
  }

  /**
   * Tests merge with empty current array.
   *
   * @covers ::reduce
   */
  public function testMergeWithEmptyCurrent(): void {
    $current = [];
    $update = ["key" => "value"];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals(["key" => "value"], $result);
  }

  /**
   * Tests merge with empty update array.
   *
   * @covers ::reduce
   */
  public function testMergeWithEmptyUpdate(): void {
    $current = ["key" => "value"];
    $update = [];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals(["key" => "value"], $result);
  }

  /**
   * Tests overwriting nested array with scalar.
   *
   * @covers ::reduce
   */
  public function testOverwriteNestedArrayWithScalar(): void {
    $current = [
      "data" => [
        "nested" => ["a" => 1, "b" => 2],
      ],
    ];

    $update = [
      "data" => [
        "nested" => "replaced_with_string",
      ],
    ];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals("replaced_with_string", $result["data"]["nested"]);
  }

  /**
   * Tests overwriting scalar with nested array.
   *
   * @covers ::reduce
   */
  public function testOverwriteScalarWithNestedArray(): void {
    $current = [
      "data" => [
        "value" => "simple_string",
      ],
    ];

    $update = [
      "data" => [
        "value" => ["now" => "complex"],
      ],
    ];

    $result = $this->reducer->reduce($current, $update);

    $this->assertEquals(["now" => "complex"], $result["data"]["value"]);
  }

  /**
   * Tests multiple sequential merges.
   *
   * @covers ::reduce
   */
  public function testMultipleSequentialMerges(): void {
    $state = [];

    $state = $this->reducer->reduce($state, ["step1" => "complete"]);
    $state = $this->reducer->reduce($state, ["step2" => "complete"]);
    $state = $this->reducer->reduce($state, ["nested" => ["a" => 1]]);
    $state = $this->reducer->reduce($state, ["nested" => ["b" => 2]]);

    $this->assertEquals([
      "step1" => "complete",
      "step2" => "complete",
      "nested" => [
        "a" => 1,
        "b" => 2,
      ],
    ], $state);
  }

  /**
   * Tests merge with boolean values.
   *
   * @covers ::reduce
   */
  public function testMergeWithBooleanValues(): void {
    $current = ["flag" => FALSE];
    $update = ["flag" => TRUE];

    $result = $this->reducer->reduce($current, $update);

    $this->assertTrue($result["flag"]);
  }

  /**
   * Tests merge with null values in arrays.
   *
   * @covers ::reduce
   */
  public function testMergeWithNullValuesInArrays(): void {
    $current = ["key" => "value"];
    $update = ["key" => NULL, "other" => "data"];

    $result = $this->reducer->reduce($current, $update);

    // NULL in update replaces the value.
    $this->assertNull($result["key"]);
    $this->assertEquals("data", $result["other"]);
  }

  /**
   * Tests merge both arrays null.
   *
   * @covers ::reduce
   */
  public function testMergeBothNull(): void {
    $result = $this->reducer->reduce(NULL, NULL);

    $this->assertNull($result);
  }

  /**
   * Tests complex real-world scenario.
   *
   * @covers ::reduce
   */
  public function testComplexRealWorldScenario(): void {
    $current = [
      "config" => [
        "database" => [
          "host" => "localhost",
          "port" => 3306,
        ],
        "cache" => [
          "enabled" => TRUE,
        ],
      ],
      "users" => [
        ["name" => "admin"],
      ],
    ];

    $update = [
      "config" => [
        "database" => [
          "password" => "secret",
        ],
        "logging" => [
          "level" => "debug",
        ],
      ],
      "users" => [
        ["name" => "guest"],
      ],
    ];

    $result = $this->reducer->reduce($current, $update);

    // Database config merged.
    $this->assertEquals("localhost", $result["config"]["database"]["host"]);
    $this->assertEquals(3306, $result["config"]["database"]["port"]);
    $this->assertEquals("secret", $result["config"]["database"]["password"]);

    // New logging config added.
    $this->assertEquals("debug", $result["config"]["logging"]["level"]);

    // Cache config preserved.
    $this->assertTrue($result["config"]["cache"]["enabled"]);

    // Users appended (numeric keys).
    $this->assertCount(2, $result["users"]);
  }

}
