<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Unit\DTO;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_stategraph\DTO\StateUpdate;

/**
 * Tests the StateUpdate DTO.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\DTO\StateUpdate
 * @group flowdrop_stategraph
 */
class StateUpdateTest extends UnitTestCase {

  /**
   * Tests constructor with all values.
   *
   * @covers ::__construct
   */
  public function testConstructor(): void {
    $data = ["key" => "value", "nested" => ["a" => 1]];

    // Note: Constructor is private, but we can test via create.
    $update = StateUpdate::create($data);

    $this->assertEquals($data, $update->getData());
    $this->assertNotNull($update->getTimestamp());
  }

  /**
   * Tests create factory method.
   *
   * @covers ::create
   */
  public function testCreate(): void {
    $data = [
      "messages" => [["role" => "user", "content" => "Hello"]],
      "data" => ["processed" => TRUE],
    ];

    $update = StateUpdate::create($data);

    $this->assertEquals($data, $update->getData());
    $this->assertNull($update->getSourceNodeId());
    $this->assertGreaterThan(time() - 5, $update->getTimestamp());
  }

  /**
   * Tests withSourceNode creates new instance with node ID.
   *
   * @covers ::withSourceNode
   */
  public function testWithSourceNode(): void {
    $update = StateUpdate::create(["key" => "value"]);
    $withNode = $update->withSourceNode("process_node");

    // Original unchanged.
    $this->assertNull($update->getSourceNodeId());

    // New instance has node ID.
    $this->assertEquals("process_node", $withNode->getSourceNodeId());

    // Data preserved.
    $this->assertEquals(["key" => "value"], $withNode->getData());
  }

  /**
   * Tests getData returns all update data.
   *
   * @covers ::getData
   */
  public function testGetData(): void {
    $data = [
      "currentNodeId" => "node_1",
      "iterationCount" => 5,
      "data" => ["result" => "success"],
    ];

    $update = StateUpdate::create($data);

    $this->assertEquals($data, $update->getData());
  }

  /**
   * Tests get retrieves specific key.
   *
   * @covers ::get
   */
  public function testGet(): void {
    $update = StateUpdate::create([
      "key1" => "value1",
      "nested" => ["a" => 1, "b" => 2],
    ]);

    $this->assertEquals("value1", $update->get("key1"));
    $this->assertEquals(["a" => 1, "b" => 2], $update->get("nested"));
  }

  /**
   * Tests get returns default for missing key.
   *
   * @covers ::get
   */
  public function testGetWithDefault(): void {
    $update = StateUpdate::create(["key" => "value"]);

    $this->assertNull($update->get("missing"));
    $this->assertEquals("default", $update->get("missing", "default"));
    $this->assertEquals(42, $update->get("missing", 42));
  }

  /**
   * Tests has checks key existence.
   *
   * @covers ::has
   */
  public function testHas(): void {
    $update = StateUpdate::create([
      "exists" => "value",
      "null_value" => NULL,
    ]);

    $this->assertTrue($update->has("exists"));
    $this->assertTrue($update->has("null_value"));
    $this->assertFalse($update->has("missing"));
  }

  /**
   * Tests getSourceNodeId.
   *
   * @covers ::getSourceNodeId
   */
  public function testGetSourceNodeId(): void {
    $update = StateUpdate::create(["key" => "value"])
      ->withSourceNode("node_xyz");

    $this->assertEquals("node_xyz", $update->getSourceNodeId());
  }

  /**
   * Tests getTimestamp.
   *
   * @covers ::getTimestamp
   */
  public function testGetTimestamp(): void {
    $before = time();
    $update = StateUpdate::create(["key" => "value"]);
    $after = time();

    $timestamp = $update->getTimestamp();

    $this->assertGreaterThanOrEqual($before, $timestamp);
    $this->assertLessThanOrEqual($after, $timestamp);
  }

  /**
   * Tests merge combines two updates.
   *
   * @covers ::merge
   */
  public function testMerge(): void {
    $update1 = StateUpdate::create([
      "key1" => "value1",
      "shared" => "first",
    ])->withSourceNode("node_1");

    $update2 = StateUpdate::create([
      "key2" => "value2",
      "shared" => "second",
    ])->withSourceNode("node_2");

    $merged = $update1->merge($update2);

    // Data from both updates.
    $this->assertEquals("value1", $merged->get("key1"));
    $this->assertEquals("value2", $merged->get("key2"));

    // Shared key uses second update's value (array_merge behavior).
    $this->assertEquals("second", $merged->get("shared"));

    // Source node from second update (or first if second is null).
    $this->assertEquals("node_2", $merged->getSourceNodeId());
  }

  /**
   * Tests merge preserves source node when second is null.
   *
   * @covers ::merge
   */
  public function testMergePreservesSourceNode(): void {
    $update1 = StateUpdate::create(["key1" => "value1"])
      ->withSourceNode("node_1");

    $update2 = StateUpdate::create(["key2" => "value2"]);

    $merged = $update1->merge($update2);

    // Source node preserved from first (second is null).
    $this->assertEquals("node_1", $merged->getSourceNodeId());
  }

  /**
   * Tests fromArray creates update from array.
   *
   * @covers ::fromArray
   */
  public function testFromArray(): void {
    $array = [
      "data" => [
        "currentNodeId" => "node_1",
        "data" => ["result" => "success"],
      ],
      "sourceNodeId" => "source_node",
      "timestamp" => 1234567890,
    ];

    $update = StateUpdate::fromArray($array);

    $this->assertEquals("node_1", $update->get("currentNodeId"));
    $this->assertEquals(["result" => "success"], $update->get("data"));
    $this->assertEquals("source_node", $update->getSourceNodeId());
    $this->assertEquals(1234567890, $update->getTimestamp());
  }

  /**
   * Tests fromArray with flat data (no wrapper).
   *
   * @covers ::fromArray
   */
  public function testFromArrayFlatData(): void {
    $array = [
      "currentNodeId" => "node_1",
      "data" => ["result" => "success"],
    ];

    $update = StateUpdate::fromArray($array);

    $this->assertEquals("node_1", $update->get("currentNodeId"));
    $this->assertEquals(["result" => "success"], $update->get("data"));
  }

  /**
   * Tests toArray serialization.
   *
   * @covers ::toArray
   */
  public function testToArray(): void {
    $update = StateUpdate::create([
      "messages" => [["role" => "user", "content" => "Test"]],
      "data" => ["key" => "value"],
    ])->withSourceNode("test_node");

    $array = $update->toArray();

    $this->assertArrayHasKey("data", $array);
    $this->assertArrayHasKey("sourceNodeId", $array);
    $this->assertArrayHasKey("timestamp", $array);

    $this->assertEquals([
      "messages" => [["role" => "user", "content" => "Test"]],
      "data" => ["key" => "value"],
    ], $array["data"]);

    $this->assertEquals("test_node", $array["sourceNodeId"]);
  }

  /**
   * Tests JSON serialization.
   *
   * @covers ::jsonSerialize
   */
  public function testJsonSerialize(): void {
    $update = StateUpdate::create(["key" => "value"])
      ->withSourceNode("json_node");

    $json = json_encode($update);
    $this->assertIsString($json);

    $decoded = json_decode($json, TRUE);
    $this->assertEquals(["key" => "value"], $decoded["data"]);
    $this->assertEquals("json_node", $decoded["sourceNodeId"]);
  }

  /**
   * Tests round-trip serialization.
   *
   * @covers ::toArray
   * @covers ::fromArray
   */
  public function testRoundTripSerialization(): void {
    $original = StateUpdate::create([
      "messages" => [["role" => "assistant", "content" => "Response"]],
      "data" => ["nested" => ["deep" => "value"]],
      "currentNodeId" => "process_node",
    ])->withSourceNode("source_node");

    $restored = StateUpdate::fromArray($original->toArray());

    $this->assertEquals($original->getData(), $restored->getData());
    $this->assertEquals($original->getSourceNodeId(), $restored->getSourceNodeId());
    $this->assertEquals($original->getTimestamp(), $restored->getTimestamp());
  }

  /**
   * Tests empty update.
   *
   * @covers ::create
   * @covers ::getData
   */
  public function testEmptyUpdate(): void {
    $update = StateUpdate::create([]);

    $this->assertEquals([], $update->getData());
    $this->assertFalse($update->has("anything"));
  }

}
