/**
 * execute.test — provisioning against a STATEFUL fake of the SmartStack admin
 * API: idempotent creates, tenant conflicts, mustChangePassword rotation,
 * stale-credential failures, and the persisted uat-users.json + gitignore.
 */
import { describe, it, expect, afterEach } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { executeProvision } from '../execute.js';
import { UatProvisionInputSchema } from '../types.js';
import type { ProvisionContext } from '../validate.js';
import { loadUsersFile, saveUsersFile, usersFileRelPath, type UatUsersFile } from '../../lib/users-file.js';
import type { FetchLike } from '../../lib/http.js';

const tmpDirs: string[] = [];
const makeTmp = (): string => {
  const dir = mkdtempSync(join(tmpdir(), 'uat-prov-'));
  tmpDirs.push(dir);
  return dir;
};
afterEach(() => {
  while (tmpDirs.length) rmSync(tmpDirs.pop()!, { recursive: true, force: true });
});

interface FakeAccount {
  password: string;
  roles: string[];
  mustChange: boolean;
}

interface FakeApiOptions {
  tenantStatus?: 201 | 409 | 500;
  roles?: { id: string; name: string }[];
  createUserStatus?: 201 | 409 | 400;
  newUsersMustChange?: boolean;
}

/** Stateful fake of the admin API: accounts persist across calls. */
function fakeApi(accounts: Map<string, FakeAccount>, opts: FakeApiOptions = {}): FetchLike {
  const json = (body: unknown, status = 200): Response => new Response(JSON.stringify(body), { status });
  return async (url, init) => {
    const body = init?.body ? (JSON.parse(String(init.body)) as Record<string, unknown>) : {};
    if (url.endsWith('/api/health')) return json({ status: 'ok' });
    if (url.endsWith('/api/auth/login')) {
      const account = accounts.get(String(body.email));
      if (!account || account.password !== body.password) return json({ message: 'Identifiants invalides' }, 401);
      return json({
        token: `tok-${body.email}`,
        refreshToken: 'r',
        user: { id: `id-${body.email}`, email: body.email, roles: account.roles, permissions: [], tenants: [{ id: 't-1', slug: 'uat' }] },
        mustChangePassword: account.mustChange,
      });
    }
    if (url.endsWith('/api/auth/change-password')) {
      const token = (init?.headers as Record<string, string>)?.Authorization ?? '';
      const email = token.replace('Bearer tok-', '');
      const account = accounts.get(email);
      if (!account || account.password !== body.currentPassword) return json({ message: 'wrong current' }, 400);
      account.password = String(body.newPassword);
      account.mustChange = false;
      return json({ message: 'ok' });
    }
    if (url.endsWith('/api/administration/tenants/b2c')) {
      const status = opts.tenantStatus ?? 201;
      return status === 201 ? json({ id: 't-1' }, 201) : json({ message: 'conflict' }, status);
    }
    if (url.endsWith('/api/administration/roles')) {
      return json(opts.roles ?? [{ id: 'r-admin', name: 'admin' }, { id: 'r-viewer', name: 'viewer' }]);
    }
    if (url.endsWith('/api/administration/users')) {
      const status = opts.createUserStatus ?? 201;
      if (status !== 201) return json({ message: 'nope' }, status);
      const roleNames = (body.roleIds as string[]).map((id) => id.replace('r-', ''));
      accounts.set(String(body.email), {
        password: String(body.password),
        roles: roleNames,
        mustChange: opts.newUsersMustChange ?? false,
      });
      return json({ id: `id-${body.email}` }, 201);
    }
    return json({ message: `unexpected ${url}` }, 500);
  };
}

function makeContext(projectRoot: string, roles: string[]): ProvisionContext {
  return {
    spec: UatProvisionInputSchema.parse({ projectPath: projectRoot }),
    projectRoot,
    application: 'administration',
    roles,
    apiUrl: 'http://api',
    apiUrlSource: 'spec',
    adminEmail: 'local.admin@smartstack.local',
    adminPassword: 'AdminPw!1',
    adminSource: 'spec',
    planRelPath: 'plan',
  };
}

const deps = (fetchImpl: FetchLike) => ({
  fetchImpl,
  nowIso: () => '2026-06-12T00:00:00.000Z',
  generatePassword: () => 'Gen!pw12345a9',
});

const adminAccount = (): Map<string, FakeAccount> =>
  new Map([['local.admin@smartstack.local', { password: 'AdminPw!1', roles: ['SupportAdmin'], mustChange: false }]]);

describe('executeProvision', () => {
  it('happy path: creates tenant + users, login-verifies, persists the gitignored users file', async () => {
    const root = makeTmp();
    const accounts = adminAccount();
    const outcome = await executeProvision(makeContext(root, ['admin', 'viewer']), deps(fakeApi(accounts)));
    expect(outcome.errors).toEqual([]);
    expect(outcome.success).toBe(true);
    expect(outcome.report.tenant.outcome).toBe('created');
    expect(outcome.report.users.map((u) => u.status)).toEqual(['created', 'created']);
    expect(outcome.report.users.every((u) => u.verified)).toBe(true);

    const file = loadUsersFile(join(root, usersFileRelPath('administration')));
    expect(file?.users.map((u) => u.role)).toEqual(['admin', 'viewer']);
    expect(file?.users[0].verifiedAt).toBe('2026-06-12T00:00:00.000Z');
    expect(existsSync(join(root, '.gitignore'))).toBe(true);
    expect(readFileSync(join(root, '.gitignore'), 'utf-8')).toContain('uat-users.json');
  });

  it('treats a tenant slug conflict as exists and proceeds', async () => {
    const outcome = await executeProvision(
      makeContext(makeTmp(), ['admin']),
      deps(fakeApi(adminAccount(), { tenantStatus: 409 })),
    );
    expect(outcome.success).toBe(true);
    expect(outcome.report.tenant.outcome).toBe('exists');
  });

  it('reports unknown roles but still provisions the known ones', async () => {
    const root = makeTmp();
    const outcome = await executeProvision(makeContext(root, ['admin', 'ghost']), deps(fakeApi(adminAccount())));
    expect(outcome.success).toBe(false);
    expect(outcome.errors.join(' ')).toContain('ghost');
    const file = loadUsersFile(join(root, usersFileRelPath('administration')));
    expect(file?.users.map((u) => u.role)).toEqual(['admin']);
  });

  it('rotates the password through change-password when the backend flags mustChangePassword', async () => {
    const root = makeTmp();
    const accounts = adminAccount();
    const outcome = await executeProvision(
      makeContext(root, ['viewer']),
      deps(fakeApi(accounts, { newUsersMustChange: true })),
    );
    expect(outcome.errors).toEqual([]);
    expect(outcome.report.users[0]).toMatchObject({ status: 'created', verified: true, passwordRotated: true });
    const file = loadUsersFile(join(root, usersFileRelPath('administration')));
    expect(file?.users[0].password).toBe('Gen!pw12345a9R1!'); // rotated suffix persisted
    expect(accounts.get('uat.viewer@uat.local')?.password).toBe('Gen!pw12345a9R1!');
  });

  it('keeps and re-verifies users from an existing file (idempotent re-run)', async () => {
    const root = makeTmp();
    const accounts = adminAccount();
    accounts.set('uat.admin@uat.local', { password: 'KnownPw!1', roles: ['admin'], mustChange: false });
    const existing: UatUsersFile = {
      version: '1',
      application: 'administration',
      apiUrl: 'http://api',
      tenant: { name: 'UAT Tenant', slug: 'uat' },
      createdAt: '2026-06-01T00:00:00.000Z',
      users: [{ role: 'admin', email: 'uat.admin@uat.local', password: 'KnownPw!1' }],
    };
    saveUsersFile(join(root, usersFileRelPath('administration')), existing);
    const outcome = await executeProvision(makeContext(root, ['admin']), deps(fakeApi(accounts)));
    expect(outcome.success).toBe(true);
    expect(outcome.report.users[0].status).toBe('kept');
    const file = loadUsersFile(join(root, usersFileRelPath('administration')));
    expect(file?.createdAt).toBe('2026-06-01T00:00:00.000Z'); // original creation stamp kept
  });

  it('fails actionably when a stored credential no longer logs in', async () => {
    const root = makeTmp();
    const accounts = adminAccount();
    accounts.set('uat.admin@uat.local', { password: 'ServerChanged!', roles: ['admin'], mustChange: false });
    saveUsersFile(join(root, usersFileRelPath('administration')), {
      version: '1',
      application: 'administration',
      apiUrl: 'http://api',
      tenant: { name: 'UAT Tenant', slug: 'uat' },
      createdAt: 'x',
      users: [{ role: 'admin', email: 'uat.admin@uat.local', password: 'OldPw!1' }],
    });
    const outcome = await executeProvision(makeContext(root, ['admin']), deps(fakeApi(accounts)));
    expect(outcome.success).toBe(false);
    expect(outcome.errors.join(' ')).toContain('no longer logs in');
  });

  it('fails fast and clear when the API is down or the admin login is wrong', async () => {
    const down: FetchLike = async () => {
      throw new Error('refused');
    };
    const downOutcome = await executeProvision(makeContext(makeTmp(), ['admin']), deps(down));
    expect(downOutcome.success).toBe(false);
    expect(downOutcome.errors[0]).toContain('ss dev up');

    const badAdmin = makeContext(makeTmp(), ['admin']);
    badAdmin.adminPassword = 'wrong';
    const badOutcome = await executeProvision(badAdmin, deps(fakeApi(adminAccount())));
    expect(badOutcome.success).toBe(false);
    expect(badOutcome.errors[0]).toContain('Security.InitialAdmin');
  });
});
