import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import express from 'express';
import request from 'supertest';
import { prismaMock } from '../../setup';
import { logger } from '../../../utils/logger';

const mockPublishConfigUpdate = jest.fn<(...args: any[]) => Promise<void>>().mockResolvedValue(undefined);
const mockRefreshConfigFromDatabase = jest.fn<(categories: unknown[]) => Promise<void>>().mockResolvedValue(undefined);

jest.mock('../../../config', () => ({
  publishConfigUpdate: mockPublishConfigUpdate,
  refreshConfigFromDatabase: mockRefreshConfigFromDatabase,
}));

let mockAdminRole = 'admin';

jest.mock('../../../middleware/auth', () => ({
  authenticateAdmin: (req: any, _res: any, next: () => void) => {
    req.admin = {
      id: '00000000-0000-4000-8000-000000000001',
      email: 'admin@example.com',
      role: mockAdminRole,
    };
    next();
  },
}));

import settingsRouter, {
  propagateConfigUpdates,
  redactSettingValue,
} from '../../../routes/admin/settings';

function createApp() {
  const app = express();
  app.use(express.json());
  app.use('/', settingsRouter);
  return app;
}

function mockPersistedSetting(key: string, value: unknown) {
  const setting = {
    id: '00000000-0000-4000-8000-000000000002',
    key,
    value,
    description: 'Test setting',
    updated_at: new Date('2026-01-01T00:00:00.000Z'),
    updated_by: '00000000-0000-4000-8000-000000000001',
  };

  (prismaMock.$transaction as any).mockImplementation(async (callback: (tx: typeof prismaMock) => Promise<unknown>) => callback(prismaMock));
  prismaMock.systemSetting.upsert.mockResolvedValue(setting as any);
  prismaMock.auditLog.create.mockResolvedValue({} as any);
  return setting;
}

describe('admin settings config propagation', () => {
  beforeEach(() => {
    mockAdminRole = 'admin';
    mockPublishConfigUpdate.mockResolvedValue(undefined);
    mockRefreshConfigFromDatabase.mockResolvedValue(undefined);
    jest.spyOn(logger, 'error').mockImplementation(() => logger);
  });

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

  it('refreshes the API and publishes deduplicated categories to workers', async () => {
    const categories = await propagateConfigUpdates([
      'gemini_api_key',
      'paypal_client_id',
      'pricing_per_1k_characters',
      'gemini_api_key',
    ], {
      gemini_api_key: '2026-01-01T00:00:00.000Z',
      paypal_client_id: '2026-01-01T00:00:00.000Z',
      pricing_per_1k_characters: '2026-01-01T00:00:00.000Z',
    });

    expect(categories).toEqual(['gemini', 'paypal', 'pricing']);
    expect(mockRefreshConfigFromDatabase).toHaveBeenCalledWith(['gemini', 'paypal', 'pricing']);
    expect(mockPublishConfigUpdate).toHaveBeenCalledWith(
      ['gemini', 'paypal', 'pricing'],
      undefined,
      {
        settingVersions: {
          gemini_api_key: '2026-01-01T00:00:00.000Z',
          paypal_client_id: '2026-01-01T00:00:00.000Z',
          pricing_per_1k_characters: '2026-01-01T00:00:00.000Z',
        },
      }
    );
  });

  it('does not publish settings without a dynamic config mapping', async () => {
    await expect(propagateConfigUpdates(['maintenance.enabled'])).resolves.toEqual([]);
    expect(mockRefreshConfigFromDatabase).not.toHaveBeenCalled();
    expect(mockPublishConfigUpdate).not.toHaveBeenCalled();
  });

  it('rejects when worker publication fails', async () => {
    mockPublishConfigUpdate.mockRejectedValueOnce(new Error('No workers subscribed'));

    await expect(propagateConfigUpdates(
      ['gemini_api_key'],
      { gemini_api_key: '2026-01-01T00:00:00.000Z' }
    )).rejects.toThrow(
      'Configuration propagation failed'
    );
  });

  it('still publishes when the local refresh fails', async () => {
    mockRefreshConfigFromDatabase.mockRejectedValueOnce(new Error('Local refresh failed'));

    await expect(propagateConfigUpdates(
      ['gemini_api_key'],
      { gemini_api_key: '2026-01-01T00:00:00.000Z' }
    )).rejects.toThrow(
      'Configuration propagation failed'
    );
    expect(mockPublishConfigUpdate).toHaveBeenCalledWith(
      ['gemini'],
      undefined,
      { settingVersions: { gemini_api_key: '2026-01-01T00:00:00.000Z' } }
    );
  });

  it('returns 503 after persisting settings when propagation fails', async () => {
    mockPersistedSetting('gemini_api_key', 'persisted-secret');
    mockPublishConfigUpdate.mockRejectedValueOnce(new Error('No workers subscribed'));

    const response = await request(createApp())
      .patch('/')
      .send({ settings: [{ key: 'gemini_api_key', value: 'persisted-secret' }] });

    expect(response.status).toBe(503);
    expect(response.body).toMatchObject({
      error: true,
      code: 'CONFIG_PROPAGATION_FAILED',
      settingsPersisted: true,
    });
    expect(response.body.configRefreshed).toBeUndefined();
    expect(prismaMock.systemSetting.upsert).toHaveBeenCalledTimes(1);
  });

  it('returns 503 after persisting a single setting when propagation fails', async () => {
    mockPersistedSetting('paypal_client_secret', 'persisted-secret');
    mockPublishConfigUpdate.mockRejectedValueOnce(new Error('No workers subscribed'));

    const response = await request(createApp())
      .put('/paypal_client_secret')
      .send({ value: 'persisted-secret' });

    expect(response.status).toBe(503);
    expect(response.body).toMatchObject({
      error: true,
      code: 'CONFIG_PROPAGATION_FAILED',
      settingsPersisted: true,
    });
    expect(response.body.configRefreshed).toBeUndefined();
    expect(prismaMock.systemSetting.upsert).toHaveBeenCalledTimes(1);
  });

  it('rejects support-role access to system settings', async () => {
    mockAdminRole = 'support';
    const app = createApp();

    const [getResponse, patchResponse, putResponse] = await Promise.all([
      request(app).get('/'),
      request(app).patch('/').send({ settings: [{ key: 'paypal_mode', value: 'sandbox' }] }),
      request(app).put('/paypal_mode').send({ value: 'sandbox' }),
    ]);

    expect(getResponse.status).toBe(403);
    expect(patchResponse.status).toBe(403);
    expect(putResponse.status).toBe(403);
    expect(prismaMock.systemSetting.upsert).not.toHaveBeenCalled();
  });

  it('keeps credential values write-only in GET, PATCH, and PUT responses', async () => {
    const secret = 'credential-value-that-must-not-leak';
    const persisted = mockPersistedSetting('paypal_client_secret', secret);
    prismaMock.systemSetting.findMany.mockResolvedValue([{ ...persisted, admin: null }] as any);
    prismaMock.systemSetting.findUnique.mockResolvedValue({ ...persisted, admin: null } as any);

    const app = createApp();
    const [listResponse, itemResponse, patchResponse, putResponse] = await Promise.all([
      request(app).get('/'),
      request(app).get('/paypal_client_secret'),
      request(app).patch('/').send({
        settings: [{ key: 'paypal_client_secret', value: secret }],
      }),
      request(app).put('/paypal_client_secret').send({ value: secret }),
    ]);

    for (const response of [listResponse, itemResponse, patchResponse, putResponse]) {
      expect(response.status).toBe(200);
      expect(JSON.stringify(response.body)).not.toContain(secret);
    }
    expect(listResponse.body[0]).toMatchObject({ configured: true });
    expect(itemResponse.body).toMatchObject({ configured: true });
    expect(patchResponse.body.settings[0]).toMatchObject({ configured: true });
    expect(putResponse.body).toMatchObject({ configured: true });
    expect(listResponse.body[0]).not.toHaveProperty('value');
    expect(itemResponse.body).not.toHaveProperty('value');
    expect(patchResponse.body.settings[0]).not.toHaveProperty('value');
    expect(putResponse.body).not.toHaveProperty('value');
  });

  it('redacts credential-like values from audit and application logs', async () => {
    const secret = 'paypal-secret-value-that-must-not-leak';
    mockPersistedSetting('paypal_client_secret', secret);
    const infoSpy = jest.spyOn(logger, 'info').mockImplementation(() => logger);

    const response = await request(createApp())
      .put('/paypal_client_secret')
      .send({ value: secret });

    expect(response.status).toBe(200);
    expect(redactSettingValue('paypal_client_secret', secret)).toBe('[REDACTED]');
    expect(JSON.stringify(prismaMock.auditLog.create.mock.calls)).not.toContain(secret);
    expect(JSON.stringify(infoSpy.mock.calls)).not.toContain(secret);
    expect(prismaMock.auditLog.create).toHaveBeenCalledWith(expect.objectContaining({
      data: expect.objectContaining({
        details: { key: 'paypal_client_secret', newValue: '[REDACTED]' },
      }),
    }));
  });
});
