<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Integration;

use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_stategraph\ApprovalGate\ApprovalGateHandler;
use Drupal\flowdrop_stategraph\ApprovalGate\ApprovalStatus;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\DTO\GraphState;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Integration tests for approval gate functionality.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\ApprovalGate\ApprovalGateHandler
 * @group flowdrop_stategraph
 */
class StateGraphApprovalGateTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The approval gate handler under test.
   *
   * @var \Drupal\flowdrop_stategraph\ApprovalGate\ApprovalGateHandler
   */
  private ApprovalGateHandler $handler;

  /**
   * Mock event dispatcher.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy<\Symfony\Component\EventDispatcher\EventDispatcherInterface>
   */
  private $eventDispatcher;

  /**
   * Dispatched events tracker.
   *
   * @var array<array{event: mixed, name: string}>
   */
  private array $dispatchedEvents = [];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->dispatchedEvents = [];

    // Note: For void methods, just stub without willReturn().
    $logger = $this->prophesize(LoggerInterface::class);
    $logger->info(Argument::cetera());
    $logger->warning(Argument::cetera());
    $logger->debug(Argument::cetera());

    $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
    $dispatchedEvents = &$this->dispatchedEvents;
    $this->eventDispatcher->dispatch(Argument::any(), Argument::type("string"))
      ->will(function ($args) use (&$dispatchedEvents) {
        $dispatchedEvents[] = [
          "event" => $args[0],
          "name" => $args[1],
        ];
        return $args[0];
      });

    $this->handler = new ApprovalGateHandler(
      $logger->reveal(),
      $this->eventDispatcher->reveal(),
    );

    // Clear any previous requests.
    $this->handler->clear();
  }

  /**
   * {@inheritdoc}
   */
  protected function tearDown(): void {
    $this->handler->clear();
    parent::tearDown();
  }

  /**
   * Creates a test checkpoint.
   *
   * @param string $nodeId
   *   The node ID.
   * @param array<string, mixed> $stateData
   *   Additional state data.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\Checkpoint
   *   A test checkpoint.
   */
  private function createTestCheckpoint(
    string $nodeId = "approval_node",
    array $stateData = [],
  ): Checkpoint {
    $state = new GraphState(
      threadId: "thread_test",
      data: array_merge(["test" => "data"], $stateData),
      iterationCount: 5,
    );

    return Checkpoint::create($state, $nodeId);
  }

  /**
   * Tests creating an approval request pauses execution.
   *
   * @covers ::createRequest
   */
  public function testApprovalGatePausesExecution(): void {
    $checkpoint = $this->createTestCheckpoint("pause_node");

    $requestId = $this->handler->createRequest(
      "pause_node",
      $checkpoint,
      ["reason" => "Needs human review"]
    );

    // Request should be created.
    $this->assertStringStartsWith("approval_", $requestId);

    // Status should be pending.
    $status = $this->handler->getStatus($requestId);
    $this->assertEquals(ApprovalStatus::Pending, $status);

    // Event should be dispatched.
    $this->assertCount(1, $this->dispatchedEvents);
    $this->assertEquals(
      "flowdrop_stategraph.approval_requested",
      $this->dispatchedEvents[0]["name"]
    );
  }

  /**
   * Tests checkpoint is saved before pause.
   *
   * @covers ::createRequest
   * @covers ::getCheckpoint
   */
  public function testApprovalGateCreatesCheckpoint(): void {
    $checkpoint = $this->createTestCheckpoint("checkpoint_node", [
      "important" => "data_to_preserve",
    ]);

    $requestId = $this->handler->createRequest("checkpoint_node", $checkpoint);

    // Should be able to retrieve the checkpoint.
    $retrieved = $this->handler->getCheckpoint($requestId);

    $this->assertNotNull($retrieved);
    $this->assertEquals($checkpoint->id, $retrieved->id);
    $this->assertEquals($checkpoint->threadId, $retrieved->threadId);
    $this->assertEquals(
      "data_to_preserve",
      $retrieved->state->data["important"]
    );
  }

  /**
   * Tests execution can resume after approval.
   *
   * @covers ::createRequest
   * @covers ::approve
   * @covers ::getStatus
   */
  public function testApprovalGateResumeAfterApproval(): void {
    $checkpoint = $this->createTestCheckpoint("resume_node");

    $requestId = $this->handler->createRequest("resume_node", $checkpoint);

    // Initially pending.
    $this->assertEquals(ApprovalStatus::Pending, $this->handler->getStatus($requestId));

    // Approve the request.
    $result = $this->handler->approve($requestId, 123, "Looks good");

    $this->assertTrue($result);

    // Status should now be approved.
    $this->assertEquals(ApprovalStatus::Approved, $this->handler->getStatus($requestId));

    // Approval event should be dispatched.
    $approvalEvents = array_filter(
      $this->dispatchedEvents,
      fn($e) => $e["name"] === "flowdrop_stategraph.approval_approved"
    );
    $this->assertCount(1, $approvalEvents);
  }

  /**
   * Tests state is available for inspection before approval.
   *
   * @covers ::createRequest
   * @covers ::getRequest
   */
  public function testApprovalGateStateInspection(): void {
    $checkpoint = $this->createTestCheckpoint("inspect_node", [
      "user_action" => "delete_account",
      "user_id" => 42,
      "confirmation_required" => TRUE,
    ]);

    $requestId = $this->handler->createRequest(
      "inspect_node",
      $checkpoint,
      ["description" => "User requested account deletion"]
    );

    // Get full request details for inspection.
    $details = $this->handler->getRequest($requestId);

    $this->assertNotNull($details);
    $this->assertEquals($requestId, $details["id"]);
    $this->assertEquals("inspect_node", $details["nodeId"]);
    $this->assertEquals("pending", $details["status"]);
    $this->assertEquals(
      "User requested account deletion",
      $details["context"]["description"]
    );

    // State should be accessible.
    $this->assertEquals("delete_account", $details["state"]["data"]["user_action"]);
    $this->assertEquals(42, $details["state"]["data"]["user_id"]);
    $this->assertTrue($details["state"]["data"]["confirmation_required"]);
  }

  /**
   * Tests rejecting an approval request.
   *
   * @covers ::reject
   * @covers ::getStatus
   */
  public function testApprovalGateReject(): void {
    $checkpoint = $this->createTestCheckpoint("reject_node");

    $requestId = $this->handler->createRequest("reject_node", $checkpoint);

    // Reject the request.
    $result = $this->handler->reject($requestId, 456, "Does not comply with policy");

    $this->assertTrue($result);

    // Status should be rejected.
    $this->assertEquals(ApprovalStatus::Rejected, $this->handler->getStatus($requestId));

    // Rejection event should be dispatched.
    $rejectionEvents = array_filter(
      $this->dispatchedEvents,
      fn($e) => $e["name"] === "flowdrop_stategraph.approval_rejected"
    );
    $this->assertCount(1, $rejectionEvents);

    // Verify details include rejection reason.
    $details = $this->handler->getRequest($requestId);
    $this->assertEquals("Does not comply with policy", $details["notes"]);
    $this->assertEquals(456, $details["resolvedBy"]);
  }

  /**
   * Tests cannot approve/reject non-pending requests.
   *
   * @covers ::approve
   * @covers ::reject
   */
  public function testCannotResolveNonPendingRequest(): void {
    $checkpoint = $this->createTestCheckpoint();
    $requestId = $this->handler->createRequest("test_node", $checkpoint);

    // Approve the request.
    $this->handler->approve($requestId, 1);

    // Cannot approve again.
    $result = $this->handler->approve($requestId, 2);
    $this->assertFalse($result);

    // Cannot reject an approved request.
    $result = $this->handler->reject($requestId, 2, "Too late");
    $this->assertFalse($result);

    // Status remains approved.
    $this->assertEquals(ApprovalStatus::Approved, $this->handler->getStatus($requestId));
  }

  /**
   * Tests listing pending requests.
   *
   * @covers ::listPending
   */
  public function testListPendingRequests(): void {
    // Create multiple requests.
    $cp1 = $this->createTestCheckpoint("node_1");
    $cp2 = $this->createTestCheckpoint("node_2");
    $cp3 = $this->createTestCheckpoint("node_3");

    $id1 = $this->handler->createRequest("node_1", $cp1);
    $id2 = $this->handler->createRequest("node_2", $cp2);
    $id3 = $this->handler->createRequest("node_3", $cp3);

    // All should be pending.
    $pending = $this->handler->listPending();
    $this->assertCount(3, $pending);

    // Approve one.
    $this->handler->approve($id2);

    // Now only 2 pending.
    $pending = $this->handler->listPending();
    $this->assertCount(2, $pending);

    // Should not include approved request.
    $pendingIds = array_column($pending, "id");
    $this->assertNotContains($id2, $pendingIds);
    $this->assertContains($id1, $pendingIds);
    $this->assertContains($id3, $pendingIds);
  }

  /**
   * Tests list pending respects limit.
   *
   * @covers ::listPending
   */
  public function testListPendingRespectsLimit(): void {
    // Create 5 requests.
    for ($i = 0; $i < 5; $i++) {
      $cp = $this->createTestCheckpoint("node_{$i}");
      $this->handler->createRequest("node_{$i}", $cp);
    }

    // Request only 2.
    $pending = $this->handler->listPending(2);
    $this->assertCount(2, $pending);
  }

  /**
   * Tests getStatus returns pending for unknown requests.
   *
   * @covers ::getStatus
   */
  public function testGetStatusUnknownRequest(): void {
    $status = $this->handler->getStatus("nonexistent_request");

    $this->assertEquals(ApprovalStatus::Pending, $status);
  }

  /**
   * Tests getCheckpoint returns null for unknown requests.
   *
   * @covers ::getCheckpoint
   */
  public function testGetCheckpointUnknownRequest(): void {
    $checkpoint = $this->handler->getCheckpoint("nonexistent_request");

    $this->assertNull($checkpoint);
  }

  /**
   * Tests approve returns false for unknown requests.
   *
   * @covers ::approve
   */
  public function testApproveUnknownRequest(): void {
    $result = $this->handler->approve("nonexistent_request");

    $this->assertFalse($result);
  }

  /**
   * Tests reject returns false for unknown requests.
   *
   * @covers ::reject
   */
  public function testRejectUnknownRequest(): void {
    $result = $this->handler->reject("nonexistent_request", NULL, "Test reason");

    $this->assertFalse($result);
  }

  /**
   * Tests getRequest returns null for unknown requests.
   *
   * @covers ::getRequest
   */
  public function testGetRequestUnknownRequest(): void {
    $details = $this->handler->getRequest("nonexistent_request");

    $this->assertNull($details);
  }

  /**
   * Tests complete approval workflow.
   */
  public function testCompleteApprovalWorkflow(): void {
    // Simulate workflow execution reaching an approval gate.
    $state = new GraphState(
      threadId: "workflow_thread",
      data: [
        "order_id" => "ORD-12345",
        "total_amount" => 10000.00,
        "items" => [
          ["name" => "Laptop", "price" => 10000.00],
        ],
      ],
      currentNodeId: "process_order",
      iterationCount: 3,
    );

    $checkpoint = Checkpoint::create($state, "approval_gate");

    // Create approval request.
    $requestId = $this->handler->createRequest(
      "approval_gate",
      $checkpoint,
      [
        "reason" => "High-value order requires manager approval",
        "threshold" => 5000.00,
      ]
    );

    // Verify request created with correct data.
    $details = $this->handler->getRequest($requestId);
    $this->assertEquals("workflow_thread", $details["threadId"]);
    $this->assertEquals("ORD-12345", $details["state"]["data"]["order_id"]);
    $this->assertEquals(10000.00, $details["state"]["data"]["total_amount"]);

    // Simulate manager reviewing and approving.
    $managerUserId = 42;
    $approvalNotes = "Approved - verified with customer";

    $result = $this->handler->approve($requestId, $managerUserId, $approvalNotes);
    $this->assertTrue($result);

    // Verify approval recorded correctly.
    $details = $this->handler->getRequest($requestId);
    $this->assertEquals("approved", $details["status"]);
    $this->assertEquals($managerUserId, $details["resolvedBy"]);
    $this->assertEquals($approvalNotes, $details["notes"]);
    $this->assertNotNull($details["resolvedAt"]);

    // Verify checkpoint can be retrieved for workflow resume.
    $resumeCheckpoint = $this->handler->getCheckpoint($requestId);
    $this->assertNotNull($resumeCheckpoint);
    $this->assertEquals("workflow_thread", $resumeCheckpoint->threadId);

    // Workflow can now resume from checkpoint state.
    $resumeState = $resumeCheckpoint->state;
    $this->assertEquals("ORD-12345", $resumeState->data["order_id"]);
    $this->assertEquals(3, $resumeState->iterationCount);
  }

  /**
   * Tests clear removes all requests.
   *
   * @covers ::clear
   */
  public function testClear(): void {
    // Create some requests.
    for ($i = 0; $i < 3; $i++) {
      $cp = $this->createTestCheckpoint("node_{$i}");
      $this->handler->createRequest("node_{$i}", $cp);
    }

    $this->assertCount(3, $this->handler->listPending());

    // Clear all.
    $count = $this->handler->clear();

    $this->assertEquals(3, $count);
    $this->assertCount(0, $this->handler->listPending());
  }

}
