<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_workflow\Unit\Service;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop_workflow\Diagnostic\DiagnosticContext;
use Drupal\flowdrop_workflow\Diagnostic\Finding;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\RemoveEdge;
use Drupal\flowdrop_workflow\Diagnostic\RemedyOption;
use Drupal\flowdrop_workflow\Diagnostic\RemedySelection;
use Drupal\flowdrop_workflow\Diagnostic\Severity;
use Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface;
use Drupal\flowdrop_workflow\Service\WorkflowDoctor;
use Drupal\flowdrop_workflow\Service\WorkflowMutatorInterface;
use Drupal\flowdrop_workflow\Service\WorkflowValidatorInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Tests the WorkflowDoctor orchestration.
 *
 * Covers diagnose aggregation, apply with server-rebuilt mutations, and
 * skip-vanished-finding handling.
 *
 * @coversDefaultClass \Drupal\flowdrop_workflow\Service\WorkflowDoctor
 * @group flowdrop_workflow
 */
class WorkflowDoctorTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * Builds an edge-removal finding (destructive remedy).
   */
  private function edgeFinding(string $edgeId): Finding {
    return new Finding(
      'fake',
      'R8_EDGE_SOURCE_MISSING',
      Severity::Error,
      'edge.' . $edgeId,
      'Dangling edge.',
      ['edge_id' => $edgeId],
      [new RemedyOption('remove_edge', new TranslatableMarkup('Remove edge'), new TranslatableMarkup('Delete it.'), TRUE)],
    );
  }

  /**
   * A workflow double whose graph getters and save() are stubbed.
   *
   * @return \Prophecy\Prophecy\ObjectProphecy<\Drupal\flowdrop_workflow\WorkflowDefinitionInterface>
   *   The prophecy.
   */
  private function workflow() {
    $workflow = $this->prophesize(WorkflowDefinitionInterface::class);
    $workflow->id()->willReturn('flow1');
    $workflow->getNodes()->willReturn([]);
    $workflow->getEdges()->willReturn([]);
    $workflow->getInputPorts()->willReturn([]);
    $workflow->getOutputPorts()->willReturn([]);
    return $workflow;
  }

  /**
   * Builds the doctor with the given diagnostics and mutator.
   *
   * @param list<\Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface> $diagnostics
   *   The diagnostics.
   * @param \Drupal\flowdrop_workflow\Service\WorkflowMutatorInterface $mutator
   *   The mutator.
   *
   * @return \Drupal\flowdrop_workflow\Service\WorkflowDoctor
   *   The doctor.
   */
  private function doctor(array $diagnostics, WorkflowMutatorInterface $mutator): WorkflowDoctor {
    $validator = $this->prophesize(WorkflowValidatorInterface::class);
    $validator->validate(Argument::cetera())->willReturn(ValidationResult::success());
    return new WorkflowDoctor(
      $diagnostics,
      $mutator,
      $validator->reveal(),
      $this->prophesize(LoggerInterface::class)->reveal(),
    );
  }

  /**
   * Diagnose() aggregates findings from every registered diagnostic.
   */
  public function testDiagnoseAggregates(): void {
    $d1 = new FakeDiagnostic('d1', [$this->edgeFinding('e1')]);
    $d2 = new FakeDiagnostic('d2', [$this->edgeFinding('e2')]);
    $mutator = $this->prophesize(WorkflowMutatorInterface::class);

    $report = $this->doctor([$d1, $d2], $mutator->reveal())->diagnose($this->workflow()->reveal());
    $this->assertCount(2, $report->findings());
    $this->assertFalse($report->isHealthy());
  }

  /**
   * ApplyRemedies() rebuilds mutations server-side, applies them, and saves.
   */
  public function testApplyAppliesAndSaves(): void {
    $finding = $this->edgeFinding('e1');
    $diagnostic = new FakeDiagnostic('fake', [$finding]);
    $diagnostic->mutations = [new RemoveEdge('e1')];

    $mutator = $this->prophesize(WorkflowMutatorInterface::class);
    $mutator->apply(Argument::any(), Argument::that(
      static fn (array $m): bool => count($m) === 1 && $m[0] instanceof RemoveEdge,
    ))->shouldBeCalledOnce();

    $workflow = $this->workflow();
    $workflow->save()->shouldBeCalledOnce();

    $selection = new RemedySelection($finding->id(), 'remove_edge');
    $this->doctor([$diagnostic], $mutator->reveal())
      ->applyRemedies($workflow->reveal(), [$selection]);
  }

  /**
   * A selection whose finding vanished is skipped: nothing is applied or saved.
   */
  public function testApplySkipsVanishedFinding(): void {
    $diagnostic = new FakeDiagnostic('fake', []);
    $mutator = $this->prophesize(WorkflowMutatorInterface::class);
    $mutator->apply(Argument::cetera())->shouldNotBeCalled();

    $workflow = $this->workflow();
    $workflow->save()->shouldNotBeCalled();

    $selection = new RemedySelection('deadbeefdeadbeef0000', 'remove_edge');
    $report = $this->doctor([$diagnostic], $mutator->reveal())
      ->applyRemedies($workflow->reveal(), [$selection]);
    $this->assertTrue($report->isHealthy());
  }

  /**
   * PreviewRemedies() summarises pending changes and flags destructiveness.
   */
  public function testPreviewSummarisesChanges(): void {
    $finding = $this->edgeFinding('e1');
    $diagnostic = new FakeDiagnostic('fake', [$finding]);
    $diagnostic->mutations = [new RemoveEdge('e1')];
    $mutator = $this->prophesize(WorkflowMutatorInterface::class);

    $selection = new RemedySelection($finding->id(), 'remove_edge');
    $plan = $this->doctor([$diagnostic], $mutator->reveal())
      ->previewRemedies($this->workflow()->reveal(), [$selection]);

    $this->assertTrue($plan->hasChanges());
    $this->assertTrue($plan->isDestructive());
    $this->assertSame(['Remove edge "e1".'], $plan->changes());
    $this->assertSame([], $plan->skipped());
  }

}

/**
 * A configurable diagnostic double for the doctor tests.
 */
final class FakeDiagnostic implements WorkflowDiagnosticInterface {

  /**
   * Mutations buildMutations() returns regardless of remedy.
   *
   * @var list<\Drupal\flowdrop_workflow\Diagnostic\Mutation\WorkflowMutationInterface>
   */
  public array $mutations = [];

  /**
   * Constructs a FakeDiagnostic.
   *
   * @param string $id
   *   The diagnostic id.
   * @param list<\Drupal\flowdrop_workflow\Diagnostic\Finding> $findings
   *   The findings diagnose() returns.
   */
  public function __construct(
    private readonly string $id,
    private readonly array $findings,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function id(): string {
    return $this->id;
  }

  /**
   * {@inheritdoc}
   */
  public function label(): TranslatableMarkup {
    return new TranslatableMarkup('Fake');
  }

  /**
   * {@inheritdoc}
   */
  public function diagnose(DiagnosticContext $context): array {
    return $this->findings;
  }

  /**
   * {@inheritdoc}
   */
  public function buildMutations(Finding $finding, string $remedyId, array $params, DiagnosticContext $context): array {
    return $this->mutations;
  }

}
