import { postgresTest } from "@internal/testcontainers";
import { type PrismaClient } from "@trigger.dev/database";
import slug from "slug";
import { describe, expect, vi } from "vitest";
import { ArchiveBranchService } from "~/services/archiveBranch.server";
import { UpsertBranchService } from "~/services/upsertBranch.server";
import { createTestOrgProjectWithMember, uniqueId } from "./fixtures/environmentVariablesFixtures";

vi.setConfig({ testTimeout: 60_000 });

async function createDevRoot(
  prisma: PrismaClient,
  projectId: string,
  organizationId: string,
  orgMemberId: string
) {
  return prisma.runtimeEnvironment.create({
    data: {
      slug: "dev",
      apiKey: uniqueId("tr_dev"),
      pkApiKey: uniqueId("pk_dev"),
      shortcode: uniqueId("sc"),
      projectId,
      organizationId,
      type: "DEVELOPMENT",
      orgMemberId,
      maximumConcurrencyLimit: 17,
    },
  });
}

describe("UpsertBranchService — DEVELOPMENT parent", () => {
  postgresTest(
    "creates a child branch that inherits the parent's ownership",
    async ({ prisma }) => {
      const { organization, project, user, orgMember } =
        await createTestOrgProjectWithMember(prisma);
      const devRoot = await createDevRoot(prisma, project.id, organization.id, orgMember.id);

      const result = await new UpsertBranchService(prisma).call(
        { type: "userMembership", userId: user.id },
        { projectId: project.id, env: "development", branchName: "my-feature" }
      );

      expect(result.success).toBe(true);
      if (!result.success) return;

      const { branch } = result;
      expect(branch.type).toBe("DEVELOPMENT");
      expect(branch.parentEnvironmentId).toBe(devRoot.id);
      expect(branch.branchName).toBe("my-feature");
      // The key dev-vs-preview divergence: dev branches MUST copy the parent's
      // orgMemberId (preview parents have none).
      expect(branch.orgMemberId).toBe(orgMember.id);
      // Children inherit the parent's concurrency limit at creation.
      expect(branch.maximumConcurrencyLimit).toBe(17);
      expect(branch.slug).toBe(slug(`${devRoot.slug}-my-feature`));
    }
  );

  postgresTest(
    "is idempotent — upserting the same branch returns the existing row",
    async ({ prisma }) => {
      const { organization, project, user, orgMember } =
        await createTestOrgProjectWithMember(prisma);
      await createDevRoot(prisma, project.id, organization.id, orgMember.id);
      const orgFilter = { type: "userMembership" as const, userId: user.id };
      const options = { projectId: project.id, env: "development" as const, branchName: "dup" };

      const first = await new UpsertBranchService(prisma).call(orgFilter, options);
      const second = await new UpsertBranchService(prisma).call(orgFilter, options);

      expect(first.success && second.success).toBe(true);
      if (!first.success || !second.success) return;
      expect(second.alreadyExisted).toBe(true);
      expect(second.branch.id).toBe(first.branch.id);
    }
  );

  postgresTest(
    "rejects an invalid branch name without touching the database",
    async ({ prisma }) => {
      const { organization, project, user, orgMember } =
        await createTestOrgProjectWithMember(prisma);
      await createDevRoot(prisma, project.id, organization.id, orgMember.id);

      const result = await new UpsertBranchService(prisma).call(
        { type: "userMembership", userId: user.id },
        { projectId: project.id, env: "development", branchName: "bad branch name!" }
      );

      expect(result.success).toBe(false);
    }
  );
});

describe("ArchiveBranchService — DEVELOPMENT", () => {
  postgresTest(
    "archives a dev branch and frees its slug/shortcode for reuse",
    async ({ prisma }) => {
      const { organization, project, user, orgMember } =
        await createTestOrgProjectWithMember(prisma);
      await createDevRoot(prisma, project.id, organization.id, orgMember.id);
      const orgFilter = { type: "userMembership" as const, userId: user.id };

      const created = await new UpsertBranchService(prisma).call(orgFilter, {
        projectId: project.id,
        env: "development",
        branchName: "reuse-me",
      });
      expect(created.success).toBe(true);
      if (!created.success) return;
      const originalSlug = created.branch.slug;

      const archived = await new ArchiveBranchService(prisma).call(orgFilter, {
        environmentId: created.branch.id,
      });
      expect(archived.success).toBe(true);
      if (!archived.success) return;
      expect(archived.branch.archivedAt).not.toBeNull();
      // Slug + shortcode are randomized on archive so the name can be reused.
      expect(archived.branch.slug).not.toBe(originalSlug);

      // The same branch name can now be created again (new row, deterministic slug).
      const recreated = await new UpsertBranchService(prisma).call(orgFilter, {
        projectId: project.id,
        env: "development",
        branchName: "reuse-me",
      });
      expect(recreated.success).toBe(true);
      if (!recreated.success) return;
      expect(recreated.branch.id).not.toBe(created.branch.id);
      expect(recreated.branch.slug).toBe(originalSlug);
    }
  );

  postgresTest("refuses to archive the default branch (the dev root)", async ({ prisma }) => {
    const { organization, project, user, orgMember } = await createTestOrgProjectWithMember(prisma);
    const devRoot = await createDevRoot(prisma, project.id, organization.id, orgMember.id);

    const result = await new ArchiveBranchService(prisma).call(
      { type: "userMembership", userId: user.id },
      { environmentId: devRoot.id }
    );

    expect(result.success).toBe(false);
    if (result.success) return;
    expect(result.error).toBe("The default development branch cannot be archived.");
  });
});
