import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// @inquirer/testing/vitest must be imported before modules that use @inquirer/*
import { screen } from "@inquirer/testing/vitest";
import path from "path";
import { nodeFs } from "../bundler/fs.js";
import { deploymentSelect } from "./deploymentSelect.js";
import { bigBrainAPI, bigBrainAPIMaybeThrows } from "./lib/utils/utils.js";
import { globalConfigPath } from "./lib/utils/globalConfig.js";

// Mock GET functions — can be configured per test
const mockPlatformGet = vi.fn();
const mockDeploymentGet = vi.fn();
const { mockCreateLocalDeployment } = vi.hoisted(() => ({
  mockCreateLocalDeployment: vi.fn(),
}));

// In-memory filesystem — populated in beforeEach, written to by real configure code
let testFiles: Map<string, string>;

vi.mock("../bundler/fs.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../bundler/fs.js")>();
  return {
    ...actual,
    nodeFs: {
      ...actual.nodeFs,
      exists: vi.fn(),
      readUtf8File: vi.fn(),
      writeUtf8File: vi.fn(),
      mkdir: vi.fn(),
    },
  };
});

vi.mock("./lib/utils/utils.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("./lib/utils/utils.js")>();
  return {
    ...actual,
    bigBrainAPI: vi.fn(),
    bigBrainAPIMaybeThrows: vi.fn(),
    typedPlatformClient: vi.fn(() => ({ GET: mockPlatformGet })),
    typedDeploymentClient: vi.fn(() => ({ GET: mockDeploymentGet })),
  };
});

vi.mock("dotenv", async (importOriginal) => {
  const actual = await importOriginal<typeof import("dotenv")>();
  return {
    ...actual,
    config: vi.fn(),
  };
});

vi.mock("@sentry/node", () => ({
  captureException: vi.fn(),
  close: vi.fn(),
}));

vi.mock("./deploymentCreate.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("./deploymentCreate.js")>();
  return {
    ...actual,
    createLocalDeployment: mockCreateLocalDeployment,
  };
});

vi.mock("./lib/localDeployment/run.js", async (importOriginal) => {
  const actual =
    await importOriginal<typeof import("./lib/localDeployment/run.js")>();
  return {
    ...actual,
    fetchLocalBackendStatus: vi.fn().mockResolvedValue({ kind: "running" }),
  };
});

/**
 * Routes mock Big Brain API calls by path.
 * Both `bigBrainAPI` and `bigBrainAPIMaybeThrows` delegate to this.
 */
function setupBigBrainRoutes(routes: Record<string, (data?: any) => any>) {
  const handler = (args: { path: string; data?: any }) => {
    for (const [routePath, routeHandler] of Object.entries(routes)) {
      if (args.path === routePath || args.path.startsWith(routePath)) {
        return routeHandler(args.data);
      }
    }
    throw new Error(`Unmocked Big Brain route: ${args.path}`);
  };
  vi.mocked(bigBrainAPI).mockImplementation(handler as any);
  vi.mocked(bigBrainAPIMaybeThrows).mockImplementation(handler as any);
}

describe("npx convex select", () => {
  let savedEnv: NodeJS.ProcessEnv;
  let savedIsTTY: boolean | undefined;

  beforeEach(() => {
    savedEnv = { ...process.env };
    savedIsTTY = process.stdin.isTTY;
    process.env = {};
    // Default to interactive TTY for existing tests
    process.stdin.isTTY = true as any;

    // Start with minimal filesystem: package.json for readProjectConfig fallback
    testFiles = new Map([[path.resolve("package.json"), "{}"]]);

    vi.resetAllMocks();

    // Wire up the in-memory filesystem to the nodeFs mock
    vi.mocked(nodeFs.exists).mockImplementation((p: string) =>
      testFiles.has(path.resolve(p)),
    );
    vi.mocked(nodeFs.readUtf8File).mockImplementation((p: string) => {
      const content = testFiles.get(path.resolve(p));
      if (content === undefined) {
        const err: any = new Error(
          `ENOENT: no such file or directory, open '${p}'`,
        );
        err.code = "ENOENT";
        throw err;
      }
      return content;
    });
    vi.mocked(nodeFs.writeUtf8File).mockImplementation(
      (p: string, content: string) => {
        testFiles.set(path.resolve(p), content);
      },
    );

    // typedDeploymentClient GET is called by fetchDeploymentCanonicalUrls
    mockDeploymentGet.mockResolvedValue({
      data: {
        convexCloudUrl: "https://example.convex.cloud",
        convexSiteUrl: "https://example.convex.site",
      },
    });

    // typedPlatformClient is used for reference-based deployment resolution
    vi.mocked(mockPlatformGet).mockResolvedValue({ data: undefined });
  });

  afterEach(() => {
    process.env = savedEnv;
    process.stdin.isTTY = savedIsTTY as any;
  });

  // Suppress process.exit and stderr
  beforeEach(() => {
    vi.spyOn(process, "exit").mockImplementation((() => {
      throw new Error("process.exit called");
    }) as any);
    vi.spyOn(process.stderr, "write").mockImplementation(() => true);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe("with project configured", () => {
    beforeEach(() => {
      process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123";
      testFiles.set(
        globalConfigPath(),
        JSON.stringify({ accessToken: "test-token" }),
      );
    });

    it("selects a dev deployment by name (abc-xyz-123)", async () => {
      // For a deployment name selector, the system looks up the *selected*
      // deployment's team/project (not the current CONVEX_DEPLOYMENT's).
      setupBigBrainRoutes({
        "deployment/clever-otter-890/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
        "deployment/authorize_within_current_project": () => ({
          adminKey: "dev-key",
          url: "https://clever-otter-890.convex.cloud",
          deploymentName: "clever-otter-890",
          deploymentType: "dev",
        }),
      });

      await deploymentSelect.parseAsync(["clever-otter-890"], { from: "user" });

      expect(bigBrainAPI).toHaveBeenCalledWith(
        expect.objectContaining({
          path: "deployment/authorize_within_current_project",
          data: expect.objectContaining({
            selectedDeploymentName: "clever-otter-890",
          }),
        }),
      );
      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain("CONVEX_DEPLOYMENT=dev:clever-otter-890");
      expect(envContent).toContain("team: my-team, project: my-project");
      expect(envContent).toContain(
        "CONVEX_URL=https://clever-otter-890.convex.cloud",
      );
    });

    it("selects dev deployment with 'dev' selector", async () => {
      setupBigBrainRoutes({
        "deployment/joyful-capybara-123/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
        "teams/my-team/projects/my-project/deployments": () => true,
        "deployment/authorize_within_current_project": () => ({
          adminKey: "dev-key",
          url: "https://joyful-capybara-123.convex.cloud",
          deploymentName: "joyful-capybara-123",
          deploymentType: "dev",
        }),
      });
      mockPlatformGet.mockResolvedValue({
        data: { name: "joyful-capybara-123" },
        error: undefined,
      });

      await deploymentSelect.parseAsync(["dev"], { from: "user" });

      expect(mockPlatformGet).toHaveBeenCalledWith(
        "/teams/{team_id_or_slug}/projects/{project_slug}/deployment",
        expect.objectContaining({
          params: expect.objectContaining({
            path: { team_id_or_slug: "my-team", project_slug: "my-project" },
            query: { defaultDev: true },
          }),
        }),
      );
      expect(bigBrainAPI).toHaveBeenCalledWith(
        expect.objectContaining({
          path: "deployment/authorize_within_current_project",
          data: expect.objectContaining({
            selectedDeploymentName: "joyful-capybara-123",
          }),
        }),
      );
      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain("CONVEX_DEPLOYMENT=dev:joyful-capybara-123");
    });

    it("selects dev deployment by reference 'dev/nicolas'", async () => {
      setupBigBrainRoutes({
        "deployment/joyful-capybara-123/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
        "teams/my-team/projects/my-project/deployments": () => true,
        "deployment/authorize_within_current_project": () => ({
          adminKey: "nicolas-key",
          url: "https://nicolas-dev-123.convex.cloud",
          deploymentName: "nicolas-dev-123",
          deploymentType: "dev",
        }),
      });
      mockPlatformGet.mockResolvedValue({
        data: { name: "nicolas-dev-123" },
        error: undefined,
      });

      await deploymentSelect.parseAsync(["dev/nicolas"], { from: "user" });

      expect(mockPlatformGet).toHaveBeenCalledWith(
        "/teams/{team_id_or_slug}/projects/{project_slug}/deployment",
        expect.objectContaining({
          params: expect.objectContaining({
            path: { team_id_or_slug: "my-team", project_slug: "my-project" },
            query: { reference: "dev/nicolas" },
          }),
        }),
      );
      expect(bigBrainAPI).toHaveBeenCalledWith(
        expect.objectContaining({
          path: "deployment/authorize_within_current_project",
          data: expect.objectContaining({
            selectedDeploymentName: "nicolas-dev-123",
          }),
        }),
      );
      expect(testFiles.has(path.resolve(".env.local"))).toBe(true);
    });

    it("selects a preview deployment in another project 'other-project:preview/my-feature'", async () => {
      setupBigBrainRoutes({
        "deployment/joyful-capybara-123/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
        "teams/my-team/projects/other-project/deployments": () => true,
        "deployment/authorize_within_current_project": () => ({
          adminKey: "preview-key",
          url: "https://feature-preview-123.convex.cloud",
          deploymentName: "feature-preview-123",
          deploymentType: "preview",
        }),
      });
      mockPlatformGet.mockResolvedValue({
        data: { name: "feature-preview-123" },
        error: undefined,
      });

      await deploymentSelect.parseAsync(["other-project:preview/my-feature"], {
        from: "user",
      });

      expect(mockPlatformGet).toHaveBeenCalledWith(
        "/teams/{team_id_or_slug}/projects/{project_slug}/deployment",
        expect.objectContaining({
          params: expect.objectContaining({
            path: {
              team_id_or_slug: "my-team",
              project_slug: "other-project",
            },
            query: { reference: "preview/my-feature" },
          }),
        }),
      );
      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain(
        "CONVEX_DEPLOYMENT=preview:feature-preview-123",
      );
    });

    describe("prod deployment restrictions", () => {
      it("fails with an error message when 'prod' selector is used", async () => {
        setupBigBrainRoutes({
          "deployment/joyful-capybara-123/team_and_project": () => ({
            team: "my-team",
            project: "my-project",
            teamId: 1,
            projectId: 1,
          }),
          "deployment/authorize_prod": () => ({
            adminKey: "prod-key",
            url: "https://graceful-puffin-456.convex.cloud",
            deploymentName: "graceful-puffin-456",
            deploymentType: "prod",
          }),
        });

        await expect(
          deploymentSelect.parseAsync(["prod"], { from: "user" }),
        ).rejects.toThrow();

        expect(process.stderr.write).toHaveBeenCalledWith(
          expect.stringContaining("--deployment prod"),
        );
        expect(testFiles.has(path.resolve(".env.local"))).toBe(false);
      });

      it("fails with an error message when a deployment name resolves to a prod deployment", async () => {
        setupBigBrainRoutes({
          "deployment/graceful-puffin-456/team_and_project": () => ({
            team: "my-team",
            project: "my-project",
            teamId: 1,
            projectId: 1,
          }),
          "deployment/authorize_within_current_project": () => ({
            adminKey: "prod-key",
            url: "https://graceful-puffin-456.convex.cloud",
            deploymentName: "graceful-puffin-456",
            deploymentType: "prod",
          }),
        });

        await expect(
          deploymentSelect.parseAsync(["graceful-puffin-456"], {
            from: "user",
          }),
        ).rejects.toThrow();

        expect(process.stderr.write).toHaveBeenCalledWith(
          expect.stringContaining("--deployment graceful-puffin-456"),
        );
        expect(testFiles.has(path.resolve(".env.local"))).toBe(false);
      });
    });

    describe("side effects on successful selection", () => {
      it("fetches the canonical URLs using the resolved deployment credentials", async () => {
        setupBigBrainRoutes({
          "deployment/joyful-capybara-123/team_and_project": () => ({
            team: "my-team",
            project: "my-project",
            teamId: 1,
            projectId: 1,
          }),
          "teams/my-team/projects/my-project/deployments": () => true,
          "deployment/authorize_within_current_project": () => ({
            adminKey: "dev-key",
            url: "https://joyful-capybara-123.convex.cloud",
            deploymentName: "joyful-capybara-123",
            deploymentType: "dev",
          }),
        });
        mockPlatformGet.mockResolvedValue({
          data: { name: "joyful-capybara-123" },
          error: undefined,
        });

        await deploymentSelect.parseAsync(["dev"], { from: "user" });

        expect(mockDeploymentGet).toHaveBeenCalledWith("/get_canonical_urls");
      });

      it("writes the fetched site URL to the env file", async () => {
        mockDeploymentGet.mockResolvedValue({
          data: {
            convexCloudUrl: "https://joyful-capybara-123.convex.cloud",
            convexSiteUrl: "https://joyful-capybara-123.convex.site",
          },
        });
        setupBigBrainRoutes({
          "deployment/joyful-capybara-123/team_and_project": () => ({
            team: "my-team",
            project: "my-project",
            teamId: 1,
            projectId: 1,
          }),
          "teams/my-team/projects/my-project/deployments": () => true,
          "deployment/authorize_within_current_project": () => ({
            adminKey: "dev-key",
            url: "https://joyful-capybara-123.convex.cloud",
            deploymentName: "joyful-capybara-123",
            deploymentType: "dev",
          }),
        });
        mockPlatformGet.mockResolvedValue({
          data: { name: "joyful-capybara-123" },
          error: undefined,
        });

        await deploymentSelect.parseAsync(["dev"], { from: "user" });

        const envContent = testFiles.get(path.resolve(".env.local"))!;
        expect(envContent).toContain(
          "CONVEX_SITE_URL=https://joyful-capybara-123.convex.site",
        );
      });

      it("uses the existing deployment name to detect unchanged selections", async () => {
        // deploymentNameFromSelection(currentSelection) extracts "joyful-capybara-123"
        // from process.env.CONVEX_DEPLOYMENT ("dev:joyful-capybara-123") and passes
        // it as existingValue to configure so it can detect whether the selection changed.
        // Here we verify the full chain ran: the correct name is written to .env.local.
        setupBigBrainRoutes({
          "deployment/joyful-capybara-123/team_and_project": () => ({
            team: "my-team",
            project: "my-project",
            teamId: 1,
            projectId: 1,
          }),
          "teams/my-team/projects/my-project/deployments": () => true,
          "deployment/authorize_within_current_project": () => ({
            adminKey: "dev-key",
            url: "https://joyful-capybara-123.convex.cloud",
            deploymentName: "joyful-capybara-123",
            deploymentType: "dev",
          }),
        });
        mockPlatformGet.mockResolvedValue({
          data: { name: "joyful-capybara-123" },
          error: undefined,
        });

        await deploymentSelect.parseAsync(["dev"], { from: "user" });

        const envContent = testFiles.get(path.resolve(".env.local"))!;
        expect(envContent).toContain(
          "CONVEX_DEPLOYMENT=dev:joyful-capybara-123",
        );
      });
    });

    it("selects local deployment with 'local' selector", async () => {
      testFiles.set(
        path.resolve(".convex/local/default/config.json"),
        JSON.stringify({
          ports: { cloud: 3210, site: 3211 },
          adminKey: "local-key",
          backendVersion: "1.0.0",
          deploymentName: "local-my_team-my_project-abc",
        }),
      );
      // The local deployment name is looked up via Big Brain for project
      // access checks (checkAccessToSelectedProject)
      // FIXME We should probably avoid the Big Brain call here so that it works offline
      setupBigBrainRoutes({
        "deployment/local-my_team-my_project-abc/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
      });

      await deploymentSelect.parseAsync(["local"], { from: "user" });

      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain(
        "CONVEX_DEPLOYMENT=local:local-my_team-my_project-abc",
      );
      // Site URL fetch should be skipped for local deployments
      expect(envContent).not.toContain("CONVEX_SITE_URL");
    });

    it("creates a local deployment when user approves the 'Create one now?' prompt", async () => {
      mockCreateLocalDeployment.mockResolvedValue(undefined);

      const promise = deploymentSelect.parseAsync(["local"], { from: "user" });

      await screen.next();
      expect(screen.getScreen()).toContain(
        "No local deployment found. Create one now?",
      );
      screen.keypress("y");
      screen.keypress("enter");

      await promise;

      expect(mockCreateLocalDeployment).toHaveBeenCalledTimes(1);
    });

    it("fails with 'No local deployment found' when no local config exists and stdin is not a TTY", async () => {
      const previousIsTTY = process.stdin.isTTY;
      process.stdin.isTTY = false as any;
      try {
        await expect(
          deploymentSelect.parseAsync(["local"], { from: "user" }),
        ).rejects.toThrow();

        expect(process.stderr.write).toHaveBeenCalledWith(
          expect.stringContaining("No local deployment found"),
        );
        expect(mockCreateLocalDeployment).not.toHaveBeenCalled();
      } finally {
        process.stdin.isTTY = previousIsTTY as any;
      }
    });
  });

  describe("without project configured", () => {
    beforeEach(() => {
      delete process.env.CONVEX_DEPLOYMENT;
      testFiles.set(
        globalConfigPath(),
        JSON.stringify({ accessToken: "test-token" }),
      );
    });

    it("fails with 'No project configured' for the 'dev' selector", async () => {
      await expect(
        deploymentSelect.parseAsync(["dev"], { from: "user" }),
      ).rejects.toThrow();

      expect(process.stderr.write).toHaveBeenCalledWith(
        expect.stringContaining("No project configured"),
      );
    });

    it("fails with 'No project configured' for a simple reference selector", async () => {
      await expect(
        deploymentSelect.parseAsync(["staging"], { from: "user" }),
      ).rejects.toThrow();

      expect(process.stderr.write).toHaveBeenCalledWith(
        expect.stringContaining("No project configured"),
      );
    });

    it("fails with 'No project configured' for a project:reference selector (needs team context)", async () => {
      await expect(
        deploymentSelect.parseAsync(["my-project:staging"], { from: "user" }),
      ).rejects.toThrow();

      expect(process.stderr.write).toHaveBeenCalledWith(
        expect.stringContaining("No project configured"),
      );
    });

    it("succeeds with a fully-qualified 'team:project:ref' selector", async () => {
      setupBigBrainRoutes({
        "deployment/authorize_within_current_project": () => ({
          adminKey: "fq-key",
          url: "https://fully-qualified-123.convex.cloud",
          deploymentName: "fully-qualified-123",
          deploymentType: "dev",
        }),
        "teams/my-team/projects/my-project/deployments": () => true,
      });
      mockPlatformGet.mockResolvedValue({
        data: { name: "fully-qualified-123" },
        error: undefined,
      });

      await deploymentSelect.parseAsync(["my-team:my-project:staging"], {
        from: "user",
      });

      expect(mockPlatformGet).toHaveBeenCalledWith(
        "/teams/{team_id_or_slug}/projects/{project_slug}/deployment",
        expect.objectContaining({
          params: expect.objectContaining({
            path: { team_id_or_slug: "my-team", project_slug: "my-project" },
            query: { reference: "staging" },
          }),
        }),
      );
      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain("CONVEX_DEPLOYMENT=dev:fully-qualified-123");
    });

    it("succeeds with a deployment name directly (does not need project context)", async () => {
      // Deployment names (abc-xyz-123 pattern) don't require a project to
      // already be configured — they look up their own team/project info.
      setupBigBrainRoutes({
        "deployment/clever-otter-890/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
        "deployment/authorize_within_current_project": () => ({
          adminKey: "dev-key",
          url: "https://clever-otter-890.convex.cloud",
          deploymentName: "clever-otter-890",
          deploymentType: "dev",
        }),
      });

      await deploymentSelect.parseAsync(["clever-otter-890"], { from: "user" });

      // deploymentNameFromSelection(currentSelection) returns null when there
      // is no CONVEX_DEPLOYMENT configured (kind === "chooseProject"), meaning
      // configure treats this as a brand-new selection.
      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain("CONVEX_DEPLOYMENT=dev:clever-otter-890");
    });

    it("fails with 'No project configured' for 'local' when no local deployment exists yet", async () => {
      // No `.convex/local/default/config.json` exists, so we'd normally prompt
      // "Create one now?". But since creating a local deployment requires a
      // project, we should fail up-front instead.
      await expect(
        deploymentSelect.parseAsync(["local"], { from: "user" }),
      ).rejects.toThrow();

      expect(process.stderr.write).toHaveBeenCalledWith(
        expect.stringContaining("No project configured"),
      );
      expect(process.stderr.write).not.toHaveBeenCalledWith(
        expect.stringContaining("Create one now?"),
      );
      expect(mockCreateLocalDeployment).not.toHaveBeenCalled();
    });

    it("selects local deployment without project configured", async () => {
      testFiles.set(
        path.resolve(".convex/local/default/config.json"),
        JSON.stringify({
          ports: { cloud: 3210, site: 3211 },
          adminKey: "local-key",
          backendVersion: "1.0.0",
          deploymentName: "local-my_team-my_project-abc",
        }),
      );
      setupBigBrainRoutes({
        "deployment/local-my_team-my_project-abc/team_and_project": () => ({
          team: "my-team",
          project: "my-project",
          teamId: 1,
          projectId: 1,
        }),
      });

      await deploymentSelect.parseAsync(["local"], { from: "user" });

      const envContent = testFiles.get(path.resolve(".env.local"))!;
      expect(envContent).toContain(
        "CONVEX_DEPLOYMENT=local:local-my_team-my_project-abc",
      );
    });
  });
});
