import { testCfEnv } from '../test/cf-env-state.js';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { AdminEnv } from './admin-engine.js';
import {
  MOD_CMS_PALETTE_SET,
  parseActiveSelection,
} from './theme.js';

const SITE_ORIGIN = 'https://cms.example';
const CSRF = 'csrf-token-12345678';

const mocks = vi.hoisted(() => ({
  claimInstall: vi.fn(),
  finalizeInstall: vi.fn(),
  createUser: vi.fn(),
  loginWithCredentials: vi.fn(),
  setSetting: vi.fn(),
}));

vi.mock('./install.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./install.js')>();
  return {
    ...actual,
    claimInstall: (...args: unknown[]) => mocks.claimInstall(...args),
    finalizeInstall: (...args: unknown[]) => mocks.finalizeInstall(...args),
  };
});

vi.mock('./admin-engine.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./admin-engine.js')>();
  return {
    ...actual,
    buildAuthEngine: () => ({ createUser: mocks.createUser }),
  };
});

vi.mock('./auth.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./auth.js')>();
  return {
    ...actual,
    loginWithCredentials: (...args: unknown[]) => mocks.loginWithCredentials(...args),
  };
});

vi.mock('./db.js', () => ({
  getFullDb: () => ({ db: {}, dialect: 'postgres' }),
}));

vi.mock('./settings.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./settings.js')>();
  return {
    ...actual,
    setSetting: (...args: unknown[]) => mocks.setSetting(...args),
  };
});

const env: AdminEnv & { SITE_ORIGIN: string } = {
  DATABASE_URL: 'postgres://u:p@ep-test.neon.tech/db?sslmode=require',
  AUTH_SESSION_SECRET: 'secret',
  AUTH_PEPPER: 'pepper',
  SITE_ORIGIN,
};

function makeCookies(token = CSRF) {
  return {
    get: (name: string) => (name === 'install_csrf' ? { value: token } : undefined),
  };
}

function post(
  body: Record<string, unknown>,
  opts: { origin?: string; csrfCookie?: string } = {},
) {
  const { origin = SITE_ORIGIN, csrfCookie = CSRF } = opts;
  return POST({
    request: new Request(`${SITE_ORIGIN}/api/install`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: origin,
      },
      body: JSON.stringify(body),
    }),
    cookies: makeCookies(csrfCookie),
  } as never);
}

const { POST } = await import('../pages/api/install.js');

const validAppearance = {
  themeId: 'default',
  paletteId: MOD_CMS_PALETTE_SET.defaultPaletteId,
  mode: 'system' as const,
};

const validBody = {
  siteTitle: 'My Site',
  tagline: 'A tagline',
  appearance: validAppearance,
  locale: 'en',
  adminEmail: 'admin@example.com',
  adminPassword: 'secure-pass-12',
  csrf: CSRF,
};

describe('POST /api/install', () => {
  beforeEach(() => {
  testCfEnv.current = env as never;
    vi.clearAllMocks();
    mocks.claimInstall.mockResolvedValue(true);
    mocks.createUser.mockResolvedValue(undefined);
    mocks.finalizeInstall.mockResolvedValue(undefined);
    mocks.loginWithCredentials.mockResolvedValue({ userId: 'u1' });
    mocks.setSetting.mockResolvedValue(undefined);
  });

  it('rejects validation errors with 400 before claim or createUser', async () => {
    const cases = [
      { ...validBody, siteTitle: '' },
      { ...validBody, siteTitle: 'x'.repeat(101) },
      { ...validBody, adminEmail: 'not-an-email' },
      { ...validBody, adminPassword: 'short' },
    ];
    for (const body of cases) {
      const res = await post(body);
      expect(res.status).toBe(400);
    }
    expect(mocks.claimInstall).not.toHaveBeenCalled();
    expect(mocks.createUser).not.toHaveBeenCalled();
  });

  it('clamps invalid appearance and still returns 303', async () => {
    const garbage = { themeId: 'nope', paletteId: 'nope', mode: 'nope' };
    const expected = parseActiveSelection(garbage);
    const res = await post({ ...validBody, appearance: garbage });
    expect(res.status).toBe(303);
    expect(mocks.setSetting).toHaveBeenCalledWith(
      expect.anything(),
      'theme',
      expected,
    );
  });

  it('rejects CSRF mismatch or cross-origin with 403 before claim', async () => {
    const mismatch = await post({ ...validBody, csrf: 'wrong-token-12345678' });
    expect(mismatch.status).toBe(403);
    const cross = await post(validBody, { origin: 'https://evil.test' });
    expect(cross.status).toBe(403);
    expect(mocks.claimInstall).not.toHaveBeenCalled();
  });

  it('returns 409 when claim is lost without createUser or settings writes', async () => {
    mocks.claimInstall.mockResolvedValue(false);
    const res = await post(validBody);
    expect(res.status).toBe(409);
    const body = (await res.json()) as { error?: { code?: string; message?: string } };
    expect(body.error?.code).toBe('install_in_progress');
    expect(mocks.createUser).not.toHaveBeenCalled();
    expect(mocks.setSetting).not.toHaveBeenCalled();
    expect(mocks.finalizeInstall).not.toHaveBeenCalled();
  });

  it('happy path: createUser, settings, finalize, auto-login, 303 /admin', async () => {
    const res = await post(validBody);
    expect(res.status).toBe(303);
    expect(res.headers.get('Location')).toBe('/admin');
    expect(mocks.claimInstall).toHaveBeenCalledWith(expect.anything(), expect.any(Date), 'postgres');
    expect(mocks.createUser).toHaveBeenCalledWith({
      email: 'admin@example.com',
      password: 'secure-pass-12',
      roles: ['admin'],
    });
    expect(mocks.setSetting).toHaveBeenCalledWith(expect.anything(), 'site_name', 'My Site');
    expect(mocks.setSetting).toHaveBeenCalledWith(expect.anything(), 'tagline', 'A tagline');
    expect(mocks.setSetting).toHaveBeenCalledWith(
      expect.anything(),
      'theme',
      parseActiveSelection(validAppearance),
    );
    expect(mocks.setSetting).toHaveBeenCalledWith(expect.anything(), 'locale', 'en');
    expect(mocks.finalizeInstall).toHaveBeenCalled();
    expect(mocks.loginWithCredentials).toHaveBeenCalled();
  });

  it('does not save locale when value is not a supported locale', async () => {
    const res = await post({ ...validBody, locale: 'xx-INVALID' });
    expect(res.status).toBe(303);
    expect(mocks.setSetting).not.toHaveBeenCalledWith(
      expect.anything(),
      'locale',
      'xx-INVALID',
    );
  });

  it('cosmetic settings failure after finalize still returns 303 (finalize already ran, no 500)', async () => {
    // Regression lock on the F1 reorder: finalizeInstall MUST run before the best-effort
    // cosmetic writes. A settings write throwing post-finalize must be swallowed — the install
    // already succeeded (admin exists, flag set), so it returns 303, never 500. If a future
    // refactor moves finalize back below the settings block, this test goes red.
    mocks.setSetting.mockRejectedValue(new Error('settings store unavailable'));
    const res = await post(validBody);
    expect(res.status).toBe(303);
    expect(mocks.finalizeInstall).toHaveBeenCalled();
    expect(res.headers.get('Location')).toBe('/admin');
  });

  it('createUser failure returns 409 without finalize and without raw DB leak', async () => {
    mocks.createUser.mockRejectedValue(new Error('duplicate key value violates unique constraint'));
    const res = await post(validBody);
    expect(res.status).toBe(409);
    const body = (await res.json()) as { error?: { message?: string } };
    expect(body.error?.message).toBe('That account could not be created.');
    expect(body.error?.message).not.toContain('duplicate key');
    expect(mocks.finalizeInstall).not.toHaveBeenCalled();
  });

  it('auto-login failure still returns 303 to /admin/login', async () => {
    mocks.loginWithCredentials.mockRejectedValue(new Error('signIn failed'));
    const res = await post(validBody);
    expect(res.status).toBe(303);
    expect(res.headers.get('Location')).toBe('/admin/login');
  });
});
