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 { I1LifecycleService, I2PlanningService } from "@awp/application";
import {
  authorityContext,
  unsafeOpaqueId,
  type AwpId,
  type CorrelationId,
  type OperationId,
  type PrincipalId,
  type ProjectId,
} from "@awp/contracts";
import { PostgresUnitOfWork, schema } from "@awp/persistence";

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) await db.execute(sql.raw(sqlText));
    }
  }
  return db;
}

function ids() {
  let sequence = 0;
  return {
    next<T extends AwpId>(): T {
      sequence += 1;
      return unsafeOpaqueId<T>(`i2-id-${sequence}`);
    },
  };
}

const clock = { now: () => new Date("2026-08-24T13:30:00.000Z") };

function context(projectId?: ProjectId) {
  const principal = {
    id: unsafeOpaqueId<PrincipalId>("principal:i2-owner"),
    kind: "human" as const,
    capabilities: [],
  };
  return {
    operationId: unsafeOpaqueId<OperationId>("operation:i2-planning"),
    correlationId: unsafeOpaqueId<CorrelationId>("correlation:i2-planning"),
    idempotencyKey: "i2-planning-test",
    authority: authorityContext(principal, [], projectId),
  };
}

describe("I2 PostgreSQL Planning durability", () => {
  it("persists structured planning, derived Goal readiness, immutable launch revision and audit evidence across service restart", async () => {
    const db = await database();
    const uow = new PostgresUnitOfWork(db);
    const generator = ids();
    const lifecycle = new I1LifecycleService(uow, generator, clock);
    const planning = new I2PlanningService(uow, generator, clock, lifecycle);

    const onboarded = await planning.onboardProject({
      name: "Planning Dogfood",
      repositoryUrl: "https://github.com/platform-modules/awp.git",
      intent: "Improve the dashboard planning experience for project owners",
      successCriteria: ["Owner can complete Planning and resume it after refresh"],
      mode: "simple",
      context: context(),
    });
    const projectContext = context(onboarded.projectId);
    let session = onboarded.session;

    expect(session.profile.kind).toBe("ui-heavy-application");
    expect(session.items.find((item) => item.key === "vision-alignment")).toMatchObject({
      participation: "owner-required",
      status: "pending",
    });
    expect(session.items.find((item) => item.key === "architecture")?.status).toBe("delegated");

    session = await planning.resolveItem({
      sessionId: session.id,
      itemKey: "intent",
      answer: "Ship durable owner-led Planning",
      context: projectContext,
    });
    session = await planning.setProfileKind(session.id, "existing-product-feature", projectContext);
    expect(session.items.find((item) => item.key === "intent")?.status).toBe("accepted");
    expect(session.profile.basis).toContain("explicit owner profile override");

    for (const [key, answer] of [
      ["vision-alignment", "Keep the current ProjectVision"],
      ["scope", "I2 Planning and Goal management only; no I3 autonomy work"],
      ["experience", "Owner can plan, resume, defer and launch without legacy UI"],
      ["acceptance", "Planning survives restart and exposes a complete launch brief"],
    ] as const) {
      session = await planning.resolveItem({
        sessionId: session.id,
        itemKey: key,
        answer,
        context: projectContext,
      });
    }

    expect(session.readiness).toBe("ready");
    const hierarchyBeforeRestart = await lifecycle.hierarchy(onboarded.projectId);
    expect(hierarchyBeforeRestart.goals[0]).toMatchObject({ readiness: "ready" });

    const defaults = await planning.saveDefaults(session.id, projectContext);
    expect(defaults.acceptedDecisionAnswers.architecture).toMatch(/modular-monolith/u);

    session = await planning.launch({
      sessionId: session.id,
      choice: "park",
      context: projectContext,
    });
    expect(session.status).toBe("parked");
    expect(session.planRevisionId).toBeDefined();

    const persistedTasks = await uow.transaction((tx) =>
      tx.tasks.listByProject(onboarded.projectId),
    );
    expect(persistedTasks.map((task) => task.status)).toEqual(["ready", "blocked", "blocked"]);
    expect(persistedTasks[1]?.dependencyIds).toEqual([persistedTasks[0]?.id]);
    expect(persistedTasks[2]?.dependencyIds).toEqual([persistedTasks[1]?.id]);

    const eventTypes = (
      await uow.transaction((tx) => tx.events.listByProject(onboarded.projectId))
    ).map((event) => event.type);
    expect(eventTypes).toEqual(
      expect.arrayContaining([
        "PlanningStarted",
        "PlanningItemResolved",
        "PlanningProfileChanged",
        "ProjectPlanningDefaultsUpdated",
        "PlanRevisionAccepted",
        "PlanningLaunchDispositionSelected",
      ]),
    );
    expect(await db.select().from(schema.auditRecords)).not.toHaveLength(0);
    expect(await db.select().from(schema.outboxMessages)).not.toHaveLength(0);

    const restartedUow = new PostgresUnitOfWork(db);
    const restartedLifecycle = new I1LifecycleService(restartedUow, generator, clock);
    const restartedPlanning = new I2PlanningService(
      restartedUow,
      generator,
      clock,
      restartedLifecycle,
    );
    const restored = await restartedPlanning.get(session.id);
    expect(restored).toMatchObject({
      id: session.id,
      status: "parked",
      planRevisionId: session.planRevisionId,
      revision: session.revision,
    });
    expect(restored?.items.find((item) => item.key === "scope")?.answer).toContain("I2 Planning");

    await db.$client.close();
  }, 20_000);
});
