import { describe, expect, it } from "vitest";
import {
  executeTrustedMerge,
  TrustedMergeConflictError,
  type TrustedMergeTargetChangedError,
  type TrustedMerger,
  type TrustedMergeRequest,
} from "@awp/application";
import {
  ProviderFailure,
  capability,
  unsafeOpaqueId,
  type ChangeSetId,
  type ConnectionId,
  type CorrelationId,
  type CredentialReferenceId,
  type OperationId,
  type PrincipalId,
  type ProjectId,
  type ProviderId,
  type TaskId,
  type AttemptId,
} from "@awp/contracts";
import { createCandidateManifest, type ChangeSet } from "@awp/domain";

const providerId = unsafeOpaqueId<ProviderId>("provider-github");
const changeSetId = unsafeOpaqueId<ChangeSetId>("changeset-1");
const manifest = createCandidateManifest({
  treeDigest: "tree-candidate",
  patchDigest: "patch-candidate",
  changedPaths: ["src/index.ts"],
  changes: [{ path: "src/index.ts", kind: "modify" }],
});
const publicationReference = {
  providerId,
  resourceType: "publication",
  nativeId: "publication-1",
  nativeRevision: "tree-candidate",
  observedAt: "2026-08-21T00:00:00Z",
};
const mergeReference = {
  providerId,
  resourceType: "merge",
  nativeId: "merge-1",
  nativeRevision: "merge-commit-123",
  observedAt: "2026-08-21T00:00:01Z",
};
const changeSet: ChangeSet = {
  id: changeSetId,
  projectId: unsafeOpaqueId<ProjectId>("project-1"),
  taskId: unsafeOpaqueId<TaskId>("task-1"),
  producerAttemptId: unsafeOpaqueId<AttemptId>("attempt-1"),
  baseIdentity: "base-commit",
  candidateDigest: manifest.treeDigest,
  candidateManifest: manifest,
  revision: 1,
  status: "ready-to-merge",
};
function request(): TrustedMergeRequest {
  return {
    context: {
      operationId: unsafeOpaqueId<OperationId>("operation-merge"),
      idempotencyKey: "merge:changeset-1",
      correlationId: unsafeOpaqueId<CorrelationId>("correlation-1"),
      authority: {
        principal: {
          id: unsafeOpaqueId<PrincipalId>("trusted-publisher"),
          kind: "system",
          capabilities: [capability("changeset.merge")],
        },
        capabilities: [capability("changeset.merge")],
      },
      connectionId: unsafeOpaqueId<ConnectionId>("connection-1"),
      credentialReferenceId: unsafeOpaqueId<CredentialReferenceId>("credential-1"),
    },
    changeSetId,
    repositoryKey: "repo-1",
    publicationReference,
    candidateDigest: manifest.treeDigest,
    candidateManifest: manifest,
    expectedBase: { reference: "refs/base/base-commit", revision: "base-commit" },
    expectedTarget: { reference: "refs/heads/main", revision: "target-before" },
  };
}

describe("trusted Merge mutation/reconciliation", () => {
  it("RT-007 reconciles exact successful result after ambiguous response before retry", async () => {
    let mergeCalls = 0;
    let reconcileCalls = 0;
    const merger: TrustedMerger = {
      async merge() {
        mergeCalls += 1;
        throw new ProviderFailure({
          category: "unavailable",
          retryable: true,
          providerId,
          resourceRef: publicationReference,
          safeMessage: "response lost after provider mutation",
          observedAt: "2026-08-21T00:00:01Z",
        });
      },
      async reconcile(input) {
        expect(input).toEqual(request());
        reconcileCalls += 1;
        if (reconcileCalls === 1) {
          return {
            value: { state: "not-merged" },
            references: [],
            observedAt: "2026-08-21T00:00:00Z",
          };
        }
        return {
          value: {
            state: "merged",
            result: { changeSetId, resultingRevision: "merge-commit-123", mergeReference },
          },
          references: [mergeReference],
          observedAt: "2026-08-21T00:00:02Z",
        };
      },
    };
    const result = await executeTrustedMerge(
      merger,
      changeSet,
      request(),
      async () => request().expectedTarget,
    );
    expect(result.value.resultingRevision).toBe("merge-commit-123");
    expect(result.value.mergeReference.nativeRevision).toBe("merge-commit-123");
    expect(mergeCalls).toBe(1);
    expect(reconcileCalls).toBe(2);
  });

  it("recovers an already-completed exact merge before observing the now-advanced target", async () => {
    let mergeCalls = 0;
    let observeTargetCalls = 0;
    const merger: TrustedMerger = {
      async merge() {
        mergeCalls += 1;
        throw new Error("merge must not be called for a reconciled prior result");
      },
      async reconcile() {
        return {
          value: {
            state: "merged",
            result: { changeSetId, resultingRevision: "merge-commit-123", mergeReference },
          },
          references: [mergeReference],
          observedAt: "2026-08-21T00:00:02Z",
        };
      },
    };
    const result = await executeTrustedMerge(merger, changeSet, request(), async () => {
      observeTargetCalls += 1;
      return { reference: "refs/heads/main", revision: "merge-commit-123" };
    });
    expect(result.value.resultingRevision).toBe("merge-commit-123");
    expect(mergeCalls).toBe(0);
    expect(observeTargetCalls).toBe(0);
  });

  it("prevents candidate or exact target mismatch before provider mutation", async () => {
    let mergeCalls = 0;
    const merger: TrustedMerger = {
      async merge() {
        mergeCalls += 1;
        return {
          value: { changeSetId, resultingRevision: "unexpected", mergeReference },
          references: [mergeReference],
          observedAt: "2026-08-21T00:00:00Z",
        };
      },
      async reconcile() {
        return {
          value: { state: "not-merged" },
          references: [],
          observedAt: "2026-08-21T00:00:00Z",
        };
      },
    };
    const mismatchedCandidate = { ...request(), candidateDigest: "different-tree" };
    await expect(
      executeTrustedMerge(
        merger,
        changeSet,
        mismatchedCandidate,
        async () => request().expectedTarget,
      ),
    ).rejects.toThrow(/candidate|digest/i);
    await expect(
      executeTrustedMerge(merger, changeSet, request(), async () => ({
        reference: "refs/heads/main",
        revision: "unexpected-target",
      })),
    ).rejects.toThrow(/target/i);
    expect(mergeCalls).toBe(0);
  });
  it("classifies target divergence as corrective work before provider mutation", async () => {
    let mergeCalls = 0;
    const merger: TrustedMerger = {
      async merge() {
        mergeCalls += 1;
        throw new Error("must not mutate after target divergence");
      },
      async reconcile() {
        return {
          value: { state: "not-merged" },
          references: [],
          observedAt: "2026-08-21T00:00:00Z",
        };
      },
    };
    const observed = { reference: "refs/heads/main", revision: "target-new" };
    await expect(
      executeTrustedMerge(merger, changeSet, request(), async () => observed),
    ).rejects.toMatchObject({
      name: "TrustedMergeTargetChangedError",
      observedTarget: observed,
    } satisfies Partial<TrustedMergeTargetChangedError>);
    expect(mergeCalls).toBe(0);
  });

  it("classifies clean-merge conflict separately from ambiguous transport failure", async () => {
    const merger: TrustedMerger = {
      async merge() {
        throw new ProviderFailure({
          category: "conflict-stale",
          retryable: false,
          providerId,
          safeMessage: "Repository candidate requires correction before trusted merge",
          observedAt: "2026-08-21T00:00:01Z",
        });
      },
      async reconcile() {
        return {
          value: { state: "not-merged" },
          references: [],
          observedAt: "2026-08-21T00:00:00Z",
        };
      },
    };
    await expect(
      executeTrustedMerge(merger, changeSet, request(), async () => request().expectedTarget),
    ).rejects.toBeInstanceOf(TrustedMergeConflictError);
  });

  it("keeps candidate/publication fidelity conflicts fail-closed instead of treating them as correction", async () => {
    const failure = new ProviderFailure({
      category: "conflict-stale",
      retryable: false,
      providerId,
      safeMessage: "Trusted merge publication candidate identity mismatch",
      observedAt: "2026-08-21T00:00:01Z",
    });
    const merger: TrustedMerger = {
      async merge() {
        throw failure;
      },
      async reconcile() {
        return {
          value: { state: "not-merged" },
          references: [],
          observedAt: "2026-08-21T00:00:00Z",
        };
      },
    };
    await expect(
      executeTrustedMerge(merger, changeSet, request(), async () => request().expectedTarget),
    ).rejects.toBe(failure);
  });
});
