/**
 * Tests for missing folder.meta.yaml detection during sync push,
 * the `folder add-missing` command, and the simplified `folder push` command.
 */

import { expect, test, describe } from "bun:test";
import { writeFile, mkdir, readFile, rm, mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getTestBackend, createNonAdminUser } from "./test_backend.ts";

type IsolatedWorkspaceTestContext = {
  backend: any;
  tempDir: string;
  workspaceId: string;
  runCLICommand: (
    args: string[],
    opts?: { token?: string }
  ) => Promise<{ stdout: string; stderr: string; code: number }>;
  apiRequest: (path: string, options?: RequestInit) => Promise<Response>;
};

async function createWorkspace(backend: any, workspaceId: string): Promise<void> {
  const response = await backend.apiRequest!("/api/workspaces/create", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      id: workspaceId,
      // Workspace name has a 50-char DB limit; keep it identical to the short ID.
      name: workspaceId,
    }),
  });

  if (!response.ok) {
    const error = await response.text();
    if (!error.includes("already exists") && !error.includes("duplicate")) {
      throw new Error(`Failed to create workspace ${workspaceId}: ${error}`);
    }
    return;
  }
  await response.text();
}

async function withIsolatedWorkspace(
  testFn: (ctx: IsolatedWorkspaceTestContext) => Promise<void>
): Promise<void> {
  const backend = await getTestBackend();
  const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_"));
  const workspaceId = `fmeta_${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 6)}`;
  let workspaceCreated = false;

  try {
    await createWorkspace(backend, workspaceId);
    workspaceCreated = true;

    await testFn({
      backend,
      tempDir,
      workspaceId,
      runCLICommand: (args: string[], opts?: { token?: string }) =>
        backend.runCLICommand(args, tempDir, {
          workspace: workspaceId,
          token: opts?.token,
        }),
      apiRequest: (path: string, options?: RequestInit) =>
        backend.apiRequest!(`/api/w/${workspaceId}${path}`, options),
    });
  } finally {
    if (workspaceCreated) {
      try {
        // Permanently delete (not archive): CE caps archived workspaces at 1, so archiving
        // temp workspaces would leak them back into the active set and blow the 2-workspace cap.
        const deleteResponse = await backend.apiRequest!(
          `/api/workspaces/delete/${workspaceId}`,
          { method: "DELETE" }
        );
        await deleteResponse.text();
      } catch {
        // Best-effort cleanup to avoid exceeding non-enterprise workspace limits.
      }
    }
    await rm(tempDir, { recursive: true, force: true });
  }
}

function wmillYaml(): string {
  return `defaultTs: bun\nincludes:\n  - "**"\nexcludes: []\n`;
}

// =============================================================================
// folder new — creates folder.meta.yaml with summary and display_name
// =============================================================================

describe("folder new", () => {
  test("creates folder.meta.yaml with summary and display_name", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const folderName = `newfolder${Date.now()}`;
      const result = await runCLICommand(
        ["folder", "new", folderName, "--summary", "My summary"],
      );

      expect(result.code).toEqual(0);

      const metaPath = join(tempDir, "f", folderName, "folder.meta.yaml");
      const content = await readFile(metaPath, "utf-8");
      expect(content).toContain("summary: My summary");
      expect(content).toContain(`display_name: ${folderName}`);
      expect(content).toContain("owners:");
      expect(content).toContain("extra_perms:");
    });
  });

  test("creates folder.meta.yaml with empty summary when none provided", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const folderName = `nosummary${Date.now()}`;
      const result = await runCLICommand(
        ["folder", "new", folderName],
      );

      expect(result.code).toEqual(0);

      const content = await readFile(
        join(tempDir, "f", folderName, "folder.meta.yaml"),
        "utf-8"
      );
      expect(content).toContain('summary: ""');
      expect(content).toContain(`display_name: ${folderName}`);
    });
  });

  test("fails if folder.meta.yaml already exists", async () => {
    await withIsolatedWorkspace(async ({ runCLICommand }) => {
      const folderName = `dupfolder${Date.now()}`;
      // Create first
      await runCLICommand(["folder", "new", folderName]);

      // Try again — should fail
      const result = await runCLICommand(
        ["folder", "new", folderName],
      );
      expect(result.code).not.toEqual(0);
    });
  });
});

// =============================================================================
// folder add-missing — scaffolds missing folder.meta.yaml files
// =============================================================================

describe("folder add-missing", () => {
  test("creates folder.meta.yaml for directories missing one", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      // Create two folders: one with meta, one without
      const withMeta = `withmeta${Date.now()}`;
      const withoutMeta = `withoutmeta${Date.now()}`;

      await mkdir(join(tempDir, "f", withMeta), { recursive: true });
      await writeFile(
        join(tempDir, "f", withMeta, "folder.meta.yaml"),
        'summary: ""\ndisplay_name: existing\nowners: []\nextra_perms: {}\n',
        "utf-8"
      );

      await mkdir(join(tempDir, "f", withoutMeta), { recursive: true });
      // No folder.meta.yaml for withoutMeta

      const result = await runCLICommand(
        ["folder", "add-missing", "-y"],
      );

      expect(result.code).toEqual(0);

      // withoutMeta should now have a folder.meta.yaml
      const createdMeta = await readFile(
        join(tempDir, "f", withoutMeta, "folder.meta.yaml"),
        "utf-8"
      );
      expect(createdMeta).toContain(`display_name: ${withoutMeta}`);
      expect(createdMeta).toContain("owners:");

      // withMeta should be unchanged
      const existingMeta = await readFile(
        join(tempDir, "f", withMeta, "folder.meta.yaml"),
        "utf-8"
      );
      expect(existingMeta).toContain("display_name: existing");
    });
  });

  test("reports nothing to do when all folders have meta", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const folderName = `alldone${Date.now()}`;
      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "folder.meta.yaml"),
        'summary: ""\ndisplay_name: done\nowners: []\nextra_perms: {}\n',
        "utf-8"
      );

      const result = await runCLICommand(
        ["folder", "add-missing", "-y"],
      );

      expect(result.code).toEqual(0);
      expect(result.stdout + result.stderr).toContain("Nothing to do");
    });
  });

  test("reports nothing to do when no f/ directory exists", async () => {
    await withIsolatedWorkspace(async ({ runCLICommand }) => {
      const result = await runCLICommand(
        ["folder", "add-missing", "-y"],
      );

      expect(result.code).toEqual(0);
      expect(result.stdout + result.stderr).toContain("Nothing to do");
    });
  });
});

// =============================================================================
// folder push — simplified single-arg signature
// =============================================================================

describe("folder push", () => {
  test("pushes a folder by name", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
      const folderName = `pushbyname${Date.now()}`;

      // Create local folder meta
      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "folder.meta.yaml"),
        `summary: "pushed"\ndisplay_name: "${folderName}"\nowners:\n  - "admin@windmill.dev"\nextra_perms: {}\n`,
        "utf-8"
      );

      const result = await runCLICommand(
        ["folder", "push", folderName],
      );

      expect(result.code).toEqual(0);
      expect(result.stdout + result.stderr).toContain("Folder pushed");

      // Verify via API
      const apiResp = await apiRequest(`/folders/get/${folderName}`);
      expect(apiResp.status).toEqual(200);
    });
  });

  test("fails when folder does not exist locally", async () => {
    await withIsolatedWorkspace(async ({ runCLICommand }) => {
      const result = await runCLICommand(
        ["folder", "push", "nonexistent"],
      );

      expect(result.code).not.toEqual(0);
    });
  });
});

// =============================================================================
// sync push — missing folder.meta.yaml detection
// =============================================================================

describe("sync push missing folder detection", () => {
  test("admin user gets warning but push succeeds", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const uniqueId = Date.now();
      const folderName = `nometaadmin${uniqueId}`;

      await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");

      // Create a script inside a folder WITHOUT folder.meta.yaml
      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "test_script.ts"),
        'export async function main() { return "hello"; }',
        "utf-8"
      );

      const result = await runCLICommand(
        ["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
      );

      // Admin should get a warning but push succeeds (exit 0)
      expect(result.code).toEqual(0);
      const output = result.stdout + result.stderr;
      expect(output).toContain("Missing folder.meta.yaml");
      expect(output).toContain(folderName);
      expect(output).toContain("wmill folder add-missing");
    });
  });

  test("no warning when folder.meta.yaml exists", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const uniqueId = Date.now();
      const folderName = `withmeta${uniqueId}`;

      await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");

      // Create folder WITH folder.meta.yaml
      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "folder.meta.yaml"),
        `summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`,
        "utf-8"
      );
      await writeFile(
        join(tempDir, "f", folderName, "test_script.ts"),
        'export async function main() { return "hello"; }',
        "utf-8"
      );

      const result = await runCLICommand(
        ["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
      );

      expect(result.code).toEqual(0);
      const output = result.stdout + result.stderr;
      expect(output).not.toContain("Missing folder.meta.yaml");
    });
  });

  test.skipIf(!process.env["EE_LICENSE_KEY"])("non-admin user gets error and exit code 1", async () => {
    await withIsolatedWorkspace(async ({ backend, tempDir, workspaceId, runCLICommand, apiRequest }) => {
      const nonAdminToken = await createNonAdminUser(backend, workspaceId);

      const uniqueId = Date.now();
      const folderName = `nometanonadmin${uniqueId}`;

      await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");

      // Create a script inside a folder WITHOUT folder.meta.yaml
      // First create the folder on remote so the non-admin has somewhere to push
      await apiRequest(
        "/folders/create",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            name: folderName,
            extra_perms: { "g/all": true },
          }),
        }
      );

      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "test_script.ts"),
        'export async function main() { return "hello"; }',
        "utf-8"
      );

      const result = await runCLICommand(
        ["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
        { token: nonAdminToken }
      );

      // Non-admin should get exit code 1
      expect(result.code).toEqual(1);
      const output = result.stdout + result.stderr;
      expect(output).toContain("Missing folder.meta.yaml");
      expect(output).toContain("wmill folder add-missing");
    });
  });

  test("no warning for deleted changes without folder.meta.yaml", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => {
      const uniqueId = Date.now();
      const folderName = `delfolder${uniqueId}`;

      // Create folder and script on remote via API
      await apiRequest(
        "/folders/create",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ name: folderName }),
        }
      );

      await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8");

      // Pull to get remote state, then delete the folder locally
      await runCLICommand(["sync", "pull", "--yes"]);

      // Remove the folder locally to trigger a "deleted" change
      await rm(join(tempDir, "f", folderName), { recursive: true, force: true });

      const result = await runCLICommand(
        ["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
      );

      // Should not warn about missing meta for deleted items
      const output = result.stdout + result.stderr;
      expect(output).not.toContain("Missing folder.meta.yaml");
    });
  });

  test("no warning when branch-specific folder.meta.yaml exists", async () => {
    await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => {
      const uniqueId = Date.now();
      const folderName = `branchmeta${uniqueId}`;

      // wmill.yaml with branch-specific folders configured
      await writeFile(
        join(tempDir, "wmill.yaml"),
        `defaultTs: bun\nincludes:\n  - "**"\nexcludes: []\ngitBranches:\n  dev:\n    specificItems:\n      folders:\n        - "f/${folderName}"\n`,
        "utf-8"
      );

      // Create folder with branch-specific meta only (no base folder.meta.yaml)
      await mkdir(join(tempDir, "f", folderName), { recursive: true });
      await writeFile(
        join(tempDir, "f", folderName, "folder.dev.meta.yaml"),
        `summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`,
        "utf-8"
      );
      await writeFile(
        join(tempDir, "f", folderName, "test_script.ts"),
        'export async function main() { return "hello"; }',
        "utf-8"
      );

      const result = await runCLICommand(
        ["sync", "push", "--yes", "--branch", "dev", "--includes", `f/${folderName}/**`],
      );

      expect(result.code).toEqual(0);
      const output = result.stdout + result.stderr;
      expect(output).not.toContain("Missing folder.meta.yaml");
    });
  });
});
