import { spawn, spawnSync } from 'node:child_process';
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';

type TestWorkspace = {
  binDir: string;
  homeDir: string;
  localFile: string;
  projectDir: string;
};

const scriptPath = fileURLToPath(
  new URL('../../../../scripts/infisical-account', import.meta.url),
);

let workspaces: TestWorkspace[] = [];

const createWorkspace = async (): Promise<TestWorkspace> => {
  const root = await realpathTemp();
  const homeDir = path.join(root, 'home');
  const projectDir = path.join(root, 'project');
  const binDir = path.join(root, 'bin');
  const localFile = path.join(projectDir, '.local', 'infisical.env');

  await mkdir(path.join(homeDir, '.infisical'), { recursive: true });
  await mkdir(path.dirname(localFile), { recursive: true });
  await mkdir(binDir, { recursive: true });

  const fakeInfisical = path.join(binDir, 'infisical');
  await writeFile(fakeInfisical, '#!/usr/bin/env sh\nexit 0\n');
  await chmod(fakeInfisical, 0o755);

  const workspace = { binDir, homeDir, localFile, projectDir };
  workspaces.push(workspace);
  return workspace;
};

const realpathTemp = async (): Promise<string> => {
  const base = path.join(tmpdir(), 'spoon-infisical-account-');
  const { mkdtemp } = await import('node:fs/promises');
  return mkdtemp(base);
};

const configPath = (workspace: TestWorkspace) =>
  path.join(workspace.homeDir, '.infisical', 'infisical-config.json');

const writeConfig = async (
  workspace: TestWorkspace,
  config: Record<string, unknown> | string,
) => {
  const content =
    typeof config === 'string'
      ? config
      : `${JSON.stringify(config, null, 2)}\n`;
  await writeFile(configPath(workspace), content);
};

const readConfig = async (
  workspace: TestWorkspace,
): Promise<Record<string, unknown>> =>
  JSON.parse(await readFile(configPath(workspace), 'utf8')) as Record<
    string,
    unknown
  >;

const envFor = (workspace: TestWorkspace): NodeJS.ProcessEnv => ({
  ...process.env,
  HOME: workspace.homeDir,
  PATH: `${workspace.binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
  SPOON_INFISICAL_LOCAL_FILE: workspace.localFile,
});

const runEnsure = (workspace: TestWorkspace) =>
  spawnSync(scriptPath, ['ensure'], {
    encoding: 'utf8',
    env: envFor(workspace),
  });

const writeLocalEmail = async (workspace: TestWorkspace, emailLine: string) => {
  await mkdir(path.dirname(workspace.localFile), { recursive: true });
  await writeFile(workspace.localFile, `${emailLine}\n`);
};

const twoAccountConfig = {
  loggedInUsers: [
    { email: 'work@example.com', domain: 'https://app.infisical.com' },
    { email: 'home@example.com', domain: 'https://infisical.gbrown.org' },
  ],
  loggedInUserEmail: 'work@example.com',
  LoggedInUserDomain: 'https://app.infisical.com',
};

beforeEach(() => {
  workspaces = [];
});

afterEach(async () => {
  await Promise.all(
    workspaces.map((workspace) =>
      rm(path.dirname(workspace.homeDir), { force: true, recursive: true }),
    ),
  );
});

describe('infisical-account', () => {
  test('single account no-ops without local file', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, {
      loggedInUsers: [
        { email: 'work@example.com', domain: 'https://app.infisical.com' },
      ],
      loggedInUserEmail: 'work@example.com',
      LoggedInUserDomain: 'https://app.infisical.com',
    });

    const result = runEnsure(workspace);
    const config = await readConfig(workspace);

    expect(result.status).toBe(0);
    expect(config.loggedInUserEmail).toBe('work@example.com');
    expect(config.LoggedInUserDomain).toBe('https://app.infisical.com');
  });

  test('multiple accounts require local project config', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);

    const result = runEnsure(workspace);

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain('.local/infisical.env');
    expect(result.stderr).toContain('work@example.com');
    expect(result.stderr).toContain('home@example.com');
  });

  test('multiple accounts switch to configured email', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);
    await writeLocalEmail(workspace, 'INFISICAL_EMAIL=home@example.com');

    const result = runEnsure(workspace);
    const config = await readConfig(workspace);

    expect(result.status).toBe(0);
    expect(config.loggedInUserEmail).toBe('home@example.com');
    expect(config.LoggedInUserDomain).toBe('https://infisical.gbrown.org');
  });

  test('configured email missing from local accounts fails clearly', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);
    await writeLocalEmail(workspace, 'INFISICAL_EMAIL=missing@example.com');

    const result = runEnsure(workspace);

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain(
      'not logged in locally: missing@example.com',
    );
  });

  test.each([
    'INFISICAL_EMAIL="home@example.com"',
    "INFISICAL_EMAIL='home@example.com'",
  ])('quoted email parses correctly: %s', async (line) => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);
    await writeLocalEmail(workspace, line);

    const result = runEnsure(workspace);
    const config = await readConfig(workspace);

    expect(result.status).toBe(0);
    expect(config.loggedInUserEmail).toBe('home@example.com');
  });

  test('empty email fails clearly', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);
    await writeLocalEmail(workspace, 'INFISICAL_EMAIL=');

    const result = runEnsure(workspace);

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain(
      '.local/infisical.env must contain INFISICAL_EMAIL',
    );
  });

  test('corrupt config fails clearly', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, '{not-json');

    const result = runEnsure(workspace);

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain(
      'Infisical config is invalid or missing loggedInUsers',
    );
  });

  test('concurrent ensure calls do not corrupt config', async () => {
    const workspace = await createWorkspace();
    await writeConfig(workspace, twoAccountConfig);
    await writeLocalEmail(workspace, 'INFISICAL_EMAIL=home@example.com');

    const run = () =>
      new Promise<{ status: number | null; stderr: string }>((resolve) => {
        const child = spawn(scriptPath, ['ensure'], { env: envFor(workspace) });
        let stderr = '';
        child.stderr.on('data', (chunk: Buffer) => {
          stderr += chunk.toString('utf8');
        });
        child.on('close', (status) => {
          resolve({ status, stderr });
        });
      });

    const [first, second] = await Promise.all([run(), run()]);
    const config = await readConfig(workspace);

    expect(first).toEqual({ status: 0, stderr: '' });
    expect(second).toEqual({ status: 0, stderr: '' });
    expect(config.loggedInUserEmail).toBe('home@example.com');
    expect(config.LoggedInUserDomain).toBe('https://infisical.gbrown.org');
  });
});
