<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Functional;

use Drupal\Core\Url;
use Drupal\flowdrop_workflow\Entity\FlowDropWorkflow;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\Tests\BrowserTestBase;

/**
 * Tests the two-step Workflow Doctor admin form.
 *
 * Exercises the review → confirm → apply flow against a stored workflow
 * carrying a dangling edge (an R8 finding): the report renders the finding and
 * its remedies, the confirm interstitial previews the exact change, applying
 * removes the edge and re-reports healthy, and "Back" preserves the picks.
 *
 * @group flowdrop_workflow
 */
class WorkflowDoctorFormTest extends BrowserTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['flowdrop_workflow'];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * The id of the workflow under test.
   */
  private const WORKFLOW_ID = 'doctor_test';

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->drupalLogin($this->drupalCreateUser(['edit flowdrop_workflow']));
  }

  /**
   * A clean workflow reports healthy and offers no remedy form.
   */
  public function testHealthyWorkflowReportsHealthy(): void {
    $this->createWorkflow([['id' => 'a', 'data' => ['label' => 'A']]], []);

    $this->drupalGet($this->doctorUrl());
    $assert = $this->assertSession();
    $assert->statusCodeEquals(200);
    $assert->pageTextContains('This workflow is healthy');
    // No remedy submit button on a healthy workflow.
    $assert->buttonNotExists('Review changes');
  }

  /**
   * The review step renders the finding and applies the chosen remedy.
   */
  public function testReviewConfirmAndApply(): void {
    $this->createWorkflow(
      [['id' => 'keep', 'data' => ['label' => 'Keep']]],
      [['id' => 'e1', 'source' => 'keep', 'target' => 'gone']],
    );

    $assert = $this->assertSession();

    // Step 1: review. The dangling-edge finding and its remedy are shown.
    $this->drupalGet($this->doctorUrl());
    $assert->statusCodeEquals(200);
    $assert->pageTextContains('does not exist');
    $assert->pageTextContains('Affected: edge "e1"');

    // Choose "Remove edge" and review.
    $this->getSession()->getPage()->selectFieldOption(
      'findings[error][' . $this->findingId() . '][choice]',
      'remove_edge',
    );
    $this->submitForm([], 'Review changes');

    // Step 2: confirm. The exact change is previewed.
    $assert->pageTextContains('Confirm changes');
    $assert->pageTextContains('Remove edge "e1".');
    $assert->pageTextContains('cannot be undone');

    // Apply.
    $this->submitForm([], 'Apply changes');
    $assert->pageTextContains('The workflow is now healthy.');

    // The edge is gone from storage.
    $reloaded = $this->reloadWorkflow();
    $this->assertSame([], $reloaded->getEdges());
  }

  /**
   * Going back from the confirm step preserves the picks on the review step.
   */
  public function testBackPreservesPicks(): void {
    $this->createWorkflow(
      [['id' => 'keep', 'data' => ['label' => 'Keep']]],
      [['id' => 'e1', 'source' => 'keep', 'target' => 'gone']],
    );

    $assert = $this->assertSession();
    $field = 'findings[error][' . $this->findingId() . '][choice]';

    $this->drupalGet($this->doctorUrl());
    $this->getSession()->getPage()->selectFieldOption($field, 'remove_edge');
    $this->submitForm([], 'Review changes');
    $assert->pageTextContains('Confirm changes');

    // Back returns to review; the radio is still on "remove_edge".
    $this->submitForm([], 'Back');
    $assert->pageTextContains('does not exist');
    $assert->fieldValueEquals($field, 'remove_edge');

    // And nothing was saved.
    $this->assertNotSame([], $this->reloadWorkflow()->getEdges());
  }

  /**
   * A user without edit access cannot reach the doctor.
   */
  public function testAccessDenied(): void {
    $this->createWorkflow([['id' => 'a', 'data' => ['label' => 'A']]], []);
    $this->drupalLogin($this->drupalCreateUser());
    $this->drupalGet($this->doctorUrl());
    $this->assertSession()->statusCodeEquals(403);
  }

  /**
   * Creates and saves the workflow under test.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The nodes.
   * @param array<int, array<string, mixed>> $edges
   *   The edges.
   */
  private function createWorkflow(array $nodes, array $edges): void {
    $storage = \Drupal::entityTypeManager()->getStorage('flowdrop_workflow');
    $workflow = $storage->create([
      'id' => self::WORKFLOW_ID,
      'label' => 'Doctor Test',
    ]);
    \assert($workflow instanceof FlowDropWorkflow);
    $workflow->setNodes($nodes)->setEdges($edges)->save();
  }

  /**
   * Reloads the workflow fresh from storage.
   */
  private function reloadWorkflow(): FlowDropWorkflowInterface {
    $storage = \Drupal::entityTypeManager()->getStorage('flowdrop_workflow');
    $storage->resetCache([self::WORKFLOW_ID]);
    $workflow = $storage->load(self::WORKFLOW_ID);
    \assert($workflow instanceof FlowDropWorkflowInterface);
    return $workflow;
  }

  /**
   * The doctor route URL for the workflow under test.
   */
  private function doctorUrl(): Url {
    return Url::fromRoute('flowdrop.workflow.doctor', [
      'flowdrop_workflow' => self::WORKFLOW_ID,
    ]);
  }

  /**
   * The stable finding id for the dangling-edge (R8 target-missing) finding.
   *
   * Mirrors Finding::id(): a digest of diagnosticId, code and the stable
   * locator. Hard-coding the inputs keeps the test honest about the contract.
   */
  private function findingId(): string {
    return substr(hash('sha256', 'validator:R8_EDGE_TARGET_MISSING:edge.e1'), 0, 20);
  }

}
