import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { sql } from "drizzle-orm";
import { createPgliteClient } from "@platform-modules/db/postgres/pglite";
import { describe, expect, it } from "vitest";
import type {
  AuditRecord,
  AuditRecordId,
  BusinessEvent,
  ChangeSetId,
  ConnectionId,
  CorrelationId,
  CredentialReferenceId,
  EventId,
  FactoryRunId,
  PlanRevisionId,
  OutboxMessageId,
  PrincipalId,
  ProjectId,
  ProviderId,
  ReviewId,
  SessionId,
  TaskId,
  AgentRunId,
  AttemptId,
  WorkspaceId,
} from "@awp/contracts";
import { unsafeOpaqueId } from "@awp/contracts";
import { PostgresUnitOfWork, schema } from "@awp/persistence";
import {
  assertCredentialMutationAuthority,
  credentialMutationAuthority,
  CredentialAuthorityConflictError,
  createAttemptSelectionProvenance,
  ImmutableAttemptProvenanceError,
} from "@awp/domain";

const migrationDirectory = fileURLToPath(
  new URL("../../packages/persistence/drizzle/", import.meta.url),
);

async function database() {
  const db = createPgliteClient({ schema });
  for (const name of readdirSync(migrationDirectory)
    .filter((name) => name.endsWith(".sql"))
    .sort()) {
    const migration = readFileSync(`${migrationDirectory}/${name}`, "utf8");
    for (const statement of migration.split("--> statement-breakpoint")) {
      const sqlText = statement.trim();
      if (sqlText.length > 0) await db.execute(sql.raw(sqlText));
    }
  }
  return db;
}

function fixtures(suffix: string) {
  const projectId = unsafeOpaqueId<ProjectId>(`project-${suffix}`);
  const correlationId = unsafeOpaqueId<CorrelationId>(`correlation-${suffix}`);
  const principalId = unsafeOpaqueId<PrincipalId>(`principal-${suffix}`);
  const event: BusinessEvent = {
    id: unsafeOpaqueId<EventId>(`event-${suffix}`),
    type: "project.created",
    schemaVersion: 1,
    occurredAt: "2026-08-20T00:00:00Z",
    aggregateType: "Project",
    aggregateId: projectId,
    aggregateRevision: 1,
    projectId,
    principalId,
    correlationId,
    payload: { projectId },
  };
  const audit: AuditRecord = {
    id: unsafeOpaqueId<AuditRecordId>(`audit-${suffix}`),
    occurredAt: "2026-08-20T00:00:00Z",
    principalId,
    action: "project.create",
    targetType: "Project",
    targetId: projectId,
    projectId,
    disposition: "allowed",
    correlationId,
    safeMetadata: {},
  };
  return { projectId, event, audit, outboxId: unsafeOpaqueId<OutboxMessageId>(`outbox-${suffix}`) };
}

describe("PostgreSQL foundation transaction", () => {
  it("commits authoritative state, event, audit and outbox atomically", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const f = fixtures("commit");
    await uow.transaction(async (tx) => {
      await tx.projects.insert({
        id: f.projectId,
        name: "Test Project",
        repositoryUrl: "https://example.com/repo.git",
        status: "active",
        revision: 1,
      });
      await tx.events.append(f.event);
      await tx.audit.append(f.audit);
      await tx.outbox.append({
        id: f.outboxId,
        topic: "business-event",
        payload: f.event,
        occurredAt: f.event.occurredAt,
      });
    });
    expect((await db.select().from(schema.projects)).map((row) => row.id)).toContain(f.projectId);
    expect((await db.select().from(schema.businessEvents)).map((row) => row.id)).toContain(
      f.event.id,
    );
    expect((await db.select().from(schema.auditRecords)).map((row) => row.id)).toContain(
      f.audit.id,
    );
    expect((await db.select().from(schema.outboxMessages)).map((row) => row.id)).toContain(
      f.outboxId,
    );
    await db.$client.close();
  }, 15_000);

  it("rolls all four families back when the application transaction fails", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const f = fixtures("rollback");
    await expect(
      uow.transaction(async (tx) => {
        await tx.projects.insert({
          id: f.projectId,
          name: "Test Project",
          repositoryUrl: "https://example.com/repo.git",
          status: "active",
          revision: 1,
        });
        await tx.events.append(f.event);
        throw new Error("injected failure before audit/outbox");
      }),
    ).rejects.toThrow(/injected failure/);
    expect(await db.select().from(schema.projects)).toHaveLength(0);
    expect(await db.select().from(schema.businessEvents)).toHaveLength(0);
    expect(await db.select().from(schema.auditRecords)).toHaveLength(0);
    expect(await db.select().from(schema.outboxMessages)).toHaveLength(0);
    await db.$client.close();
  });
});

describe("ADR-0009 durable operator sessions", () => {
  it("round-trips token digests, timestamps and revocation without a plaintext token column", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const principalId = unsafeOpaqueId<PrincipalId>("principal-session-persistence");
    const firstId = unsafeOpaqueId<SessionId>("session-persistence-a");
    const secondId = unsafeOpaqueId<SessionId>("session-persistence-b");
    const createdAt = "2026-08-23T00:00:00.000Z";
    const expiresAt = "2026-09-22T00:00:00.000Z";

    await uow.transaction((tx) =>
      tx.sessions.insert({
        id: firstId,
        tokenHash: "a".repeat(64),
        principalId,
        credentialVersionDigest: "d".repeat(64),
        createdAt,
        lastSeenAt: createdAt,
        expiresAt,
        userAgentDigest: "b".repeat(64),
      }),
    );
    const persisted = await uow.transaction((tx) => tx.sessions.getByTokenHash("a".repeat(64)));
    expect(persisted).toMatchObject({
      id: firstId,
      tokenHash: "a".repeat(64),
      principalId,
      credentialVersionDigest: "d".repeat(64),
      createdAt,
      lastSeenAt: createdAt,
      expiresAt,
      userAgentDigest: "b".repeat(64),
    });

    const raw = (await db.select().from(schema.sessions))[0]!;
    expect(Object.keys(raw)).not.toContain("token");
    expect(JSON.stringify(raw)).not.toContain("opaque-session-token");

    const refreshedAt = "2026-08-23T00:06:00.000Z";
    await uow.transaction((tx) => tx.sessions.updateLastSeen(firstId, refreshedAt));
    expect((await uow.transaction((tx) => tx.sessions.getById(firstId)))?.lastSeenAt).toBe(
      refreshedAt,
    );

    await uow.transaction((tx) =>
      tx.sessions.insert({
        id: secondId,
        tokenHash: "c".repeat(64),
        principalId,
        credentialVersionDigest: "d".repeat(64),
        createdAt,
        lastSeenAt: createdAt,
        expiresAt,
      }),
    );
    await expect(
      uow.transaction((tx) =>
        tx.sessions.insert({
          id: unsafeOpaqueId<SessionId>("session-persistence-duplicate"),
          tokenHash: "c".repeat(64),
          principalId,
          credentialVersionDigest: "d".repeat(64),
          createdAt,
          lastSeenAt: createdAt,
          expiresAt,
        }),
      ),
    ).rejects.toThrow();

    const revokedAt = "2026-08-23T01:00:00.000Z";
    await uow.transaction((tx) => tx.sessions.revoke(firstId, revokedAt));
    expect((await uow.transaction((tx) => tx.sessions.getById(firstId)))?.revokedAt).toBe(
      revokedAt,
    );
    await uow.transaction((tx) => tx.sessions.revokeAllByPrincipal(principalId, revokedAt));
    expect((await uow.transaction((tx) => tx.sessions.getById(secondId)))?.revokedAt).toBe(
      revokedAt,
    );
    await db.$client.close();
  }, 15_000);
});

describe("C1.x fenced credential authority", () => {
  it("rejects stale generations and fences the old writer during handoff", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const connectionId = unsafeOpaqueId<ConnectionId>("connection-authority");
    const credentialReferenceId = unsafeOpaqueId<CredentialReferenceId>("credential-authority");
    const ownerA = unsafeOpaqueId<PrincipalId>("authority-a");
    const ownerB = unsafeOpaqueId<PrincipalId>("authority-b");

    const claimed = await uow.transaction((tx) =>
      tx.credentialAuthorities.claim({ connectionId, credentialReferenceId, ownerId: ownerA }),
    );
    const tokenA = credentialMutationAuthority(claimed);
    await expect(
      uow.transaction((tx) =>
        tx.credentialAuthorities.claim({ connectionId, credentialReferenceId, ownerId: ownerB }),
      ),
    ).rejects.toMatchObject({ code: "CREDENTIAL_AUTHORITY_ALREADY_CLAIMED" });

    const handoff = await uow.transaction((tx) =>
      tx.credentialAuthorities.prepareHandoff({
        connectionId,
        credentialReferenceId,
        ownerId: ownerA,
        nextOwnerId: ownerB,
        expectedGeneration: tokenA.generation,
      }),
    );
    assertCredentialMutationAuthority(handoff, tokenA);

    const fenced = await uow.transaction((tx) =>
      tx.credentialAuthorities.fence({
        connectionId,
        credentialReferenceId,
        ownerId: ownerA,
        expectedGeneration: tokenA.generation,
      }),
    );
    expect(() => assertCredentialMutationAuthority(fenced, tokenA)).toThrow(
      CredentialAuthorityConflictError,
    );

    const transferred = await uow.transaction((tx) =>
      tx.credentialAuthorities.transfer({
        connectionId,
        credentialReferenceId,
        ownerId: ownerA,
        nextOwnerId: ownerB,
        expectedGeneration: tokenA.generation,
      }),
    );
    expect(transferred.ownerId).toBe(ownerB);
    expect(transferred.generation).toBe(tokenA.generation + 1);
    expect(() => assertCredentialMutationAuthority(transferred, tokenA)).toThrow(
      CredentialAuthorityConflictError,
    );
    await expect(
      uow.transaction((tx) =>
        tx.credentialAuthorities.fence({
          connectionId,
          credentialReferenceId,
          ownerId: ownerA,
          expectedGeneration: tokenA.generation,
        }),
      ),
    ).rejects.toMatchObject({ code: "STALE_CREDENTIAL_AUTHORITY" });
    await db.$client.close();
  });
});

describe("C1.x immutable Attempt provider provenance", () => {
  it("binds one provider/session reference and prevents retry provenance rewrites", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const attemptId = unsafeOpaqueId<AttemptId>("attempt-provider-reference");
    const providerId = unsafeOpaqueId<ProviderId>("provider-acp");
    const attempt = {
      id: attemptId,
      agentRunId: unsafeOpaqueId<AgentRunId>("agent-run-provider-reference"),
      workspaceId: unsafeOpaqueId<WorkspaceId>("workspace-provider-reference"),
      status: "running" as const,
      providerId,
      accountId: "account-1",
      model: "model-1",
      selection: createAttemptSelectionProvenance({ kind: "initial", reason: "policy selection" }),
      revision: 1,
    };
    await uow.transaction((tx) => tx.attempts.insert(attempt));
    const reference = {
      providerId,
      resourceType: "agent-session",
      nativeId: "native-session-1",
      nativeRevision: "session-revision-1",
      observedAt: "2026-08-21T00:00:00Z",
    };
    const bound = await uow.transaction((tx) =>
      tx.attempts.bindProviderReference(attemptId, reference),
    );
    expect(bound.providerReference).toMatchObject({ nativeId: "native-session-1" });
    await expect(
      uow.transaction((tx) =>
        tx.attempts.update({
          ...bound,
          selection: { ...bound.selection, reason: "rewritten reason" },
          revision: 2,
        }),
      ),
    ).rejects.toBeInstanceOf(ImmutableAttemptProvenanceError);
    await db.$client.close();
  });
});

describe("I1 durable ChangeSet publication identity", () => {
  it("round-trips repository publication/merge identities and refuses rebinding", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const providerId = unsafeOpaqueId<ProviderId>("provider:github");
    const changeSetId = unsafeOpaqueId<ChangeSetId>("changeset-publication-roundtrip");
    const publicationReference = {
      providerId,
      resourceType: "publication",
      nativeId: "publication-roundtrip",
      nativeRevision: "b".repeat(40),
      observedAt: "2026-08-22T14:00:00.000Z",
    };
    const mergeReference = {
      providerId,
      resourceType: "merge",
      nativeId: "merge-roundtrip",
      nativeRevision: "c".repeat(40),
      observedAt: "2026-08-22T14:01:00.000Z",
    };
    const candidateManifest = {
      treeDigest: "a".repeat(40),
      patchDigest: "d".repeat(64),
      changedPaths: ["README.md"],
      changes: [{ path: "README.md", kind: "modify" as const }],
    };
    await uow.transaction((tx) =>
      tx.changeSets.insert({
        id: changeSetId,
        projectId: unsafeOpaqueId<ProjectId>("project-publication-roundtrip"),
        taskId: unsafeOpaqueId<TaskId>("task-publication-roundtrip"),
        producerAttemptId: unsafeOpaqueId<AttemptId>("attempt-publication-roundtrip"),
        baseIdentity: "9".repeat(40),
        candidateDigest: candidateManifest.treeDigest,
        candidateManifest,
        diff: "diff --git a/README.md b/README.md\n",
        revision: 1,
        status: "publishing",
      }),
    );
    const collected = await uow.transaction((tx) => tx.changeSets.getById(changeSetId));
    expect(collected).toBeDefined();
    await uow.transaction((tx) =>
      tx.changeSets.update({
        ...collected!,
        repositoryKey: "platform-modules/awp",
        publicationReference,
        targetReference: "refs/heads/main",
        targetRevision: "9".repeat(40),
        status: "reviewing",
        revision: 2,
      }),
    );
    const published = await uow.transaction((tx) => tx.changeSets.getById(changeSetId));
    expect(published).toMatchObject({
      repositoryKey: "platform-modules/awp",
      targetReference: "refs/heads/main",
      targetRevision: "9".repeat(40),
      publicationReference: { nativeId: "publication-roundtrip", nativeRevision: "b".repeat(40) },
    });
    await uow.transaction((tx) =>
      tx.changeSets.update({
        ...published!,
        mergeReference,
        resultingRevision: "c".repeat(40),
        status: "merged",
        revision: 3,
      }),
    );
    const merged = await uow.transaction((tx) => tx.changeSets.getById(changeSetId));
    expect(merged).toMatchObject({
      status: "merged",
      resultingRevision: "c".repeat(40),
      mergeReference: { nativeId: "merge-roundtrip", nativeRevision: "c".repeat(40) },
    });
    await expect(
      uow.transaction((tx) =>
        tx.changeSets.update({
          ...merged!,
          repositoryKey: "other/repository",
          revision: 4,
        }),
      ),
    ).rejects.toThrow(/repository identity is immutable/);
    await db.$client.close();
  });
});

describe("I1 FactoryRun execution provenance persistence", () => {
  it("round-trips account/model selection and rejects a second FactoryRun for one PlanRevision", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const factoryRunId = unsafeOpaqueId<FactoryRunId>("factory-provenance-roundtrip");
    const projectId = unsafeOpaqueId<ProjectId>("project-factory-provenance");
    const planRevisionId = unsafeOpaqueId<PlanRevisionId>("revision-factory-provenance");
    await uow.transaction((tx) =>
      tx.factoryRuns.insert({
        id: factoryRunId,
        projectId,
        planRevisionId,
        accountId: "owner-account",
        model: "owner-model",
        status: "queued",
        revision: 1,
      }),
    );
    expect(await uow.transaction((tx) => tx.factoryRuns.getById(factoryRunId))).toMatchObject({
      accountId: "owner-account",
      model: "owner-model",
      planRevisionId,
    });
    await expect(
      uow.transaction((tx) =>
        tx.factoryRuns.insert({
          id: unsafeOpaqueId<FactoryRunId>("factory-provenance-duplicate"),
          projectId,
          planRevisionId,
          accountId: "owner-account",
          model: "owner-model",
          status: "queued",
          revision: 1,
        }),
      ),
    ).rejects.toThrow();
    await db.$client.close();
  });
});

describe("I1 independent reviewer persistence", () => {
  it("round-trips reviewer AgentRun role and Review assignment", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const factoryRunId = unsafeOpaqueId<FactoryRunId>("factory-reviewer-roundtrip");
    const taskId = unsafeOpaqueId<TaskId>("task-reviewer-roundtrip");
    const reviewerAgentRunId = unsafeOpaqueId<AgentRunId>("agent-reviewer-roundtrip");
    const reviewerPrincipalId = unsafeOpaqueId<PrincipalId>("principal-reviewer-roundtrip");
    const reviewId = unsafeOpaqueId<ReviewId>("review-roundtrip");
    const changeSetId = unsafeOpaqueId<ChangeSetId>("changeset-reviewer-roundtrip");
    await uow.transaction(async (tx) => {
      await tx.agentRuns.insert({
        id: reviewerAgentRunId,
        factoryRunId,
        taskId,
        agentPrincipalId: reviewerPrincipalId,
        role: "reviewer",
        status: "active",
        revision: 1,
      });
      await tx.reviews.insert({
        id: reviewId,
        changeSetId,
        candidateDigest: "a".repeat(40),
        reviewerPrincipalId,
        reviewerAgentRunId,
        status: "reviewing",
      });
    });
    expect(await uow.transaction((tx) => tx.agentRuns.getById(reviewerAgentRunId))).toMatchObject({
      role: "reviewer",
      agentPrincipalId: reviewerPrincipalId,
    });
    expect(await uow.transaction((tx) => tx.reviews.getById(reviewId))).toMatchObject({
      reviewerAgentRunId,
      reviewerPrincipalId,
      status: "reviewing",
    });
    await db.$client.close();
  });
});

describe("I1 project verification policy persistence", () => {
  it("round-trips the repository required-check policy", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const projectId = unsafeOpaqueId<ProjectId>("project-required-check-policy");
    await uow.transaction((tx) =>
      tx.projects.insert({
        id: projectId,
        name: "Required checks",
        repositoryUrl: "https://github.com/platform-modules/awp.git",
        requiredChecks: ["CI", "security"],
        status: "active",
        revision: 1,
      }),
    );
    expect(await uow.transaction((tx) => tx.projects.getById(projectId))).toMatchObject({
      requiredChecks: ["CI", "security"],
    });
    await db.$client.close();
  });
});

describe("I1 durable Workspace content checkpoint", () => {
  it("round-trips checkpoint collection and cleanup state", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const projectId = unsafeOpaqueId<ProjectId>("project-workspace-checkpoint");
    const workspaceId = unsafeOpaqueId<WorkspaceId>("workspace-content-checkpoint");
    await uow.transaction(async (tx) => {
      await tx.projects.insert({
        id: projectId,
        name: "Workspace checkpoint",
        repositoryUrl: "https://github.com/platform-modules/awp.git",
        requiredChecks: [],
        status: "active",
        revision: 1,
      });
      await tx.workspaces.insert({
        id: workspaceId,
        projectId,
        checkpointDigest: "d".repeat(40),
        checkpointedAt: "2026-08-23T02:30:00.000Z",
        checkpointSource: "git-tree",
        checkpointCollectedAt: "2026-08-23T02:30:01.000Z",
        cleanedAt: "2026-08-23T02:31:00.000Z",
        revision: 4,
      });
    });
    expect(await uow.transaction((tx) => tx.workspaces.getById(workspaceId))).toMatchObject({
      checkpointDigest: "d".repeat(40),
      checkpointedAt: "2026-08-23T02:30:00.000Z",
      checkpointSource: "git-tree",
      checkpointCollectedAt: "2026-08-23T02:30:01.000Z",
      cleanedAt: "2026-08-23T02:31:00.000Z",
      revision: 4,
    });
    await db.$client.close();
  });
});
