/**
 * Unit tests for the Joi validation middleware + schema definitions.
 * Verifies that valid inputs pass through and invalid inputs throw
 * ValidationError with descriptive messages.
 */
const { validateBody, schemas } = require('../../src/utilities/validate');

function mockReq(body) {
  return { body };
}

describe('validateBody middleware', () => {
  test('passes valid body through and strips unknown keys', () => {
    const schema = schemas.assetUpload;
    const req = mockReq({ filename: 'logo.png', data: 'data:image/png;base64,abc', extra: true });
    const next = jest.fn();
    validateBody(schema)(req, {}, next);
    expect(next).toHaveBeenCalled();
    expect(req.body).toHaveProperty('filename', 'logo.png');
    expect(req.body).not.toHaveProperty('extra');
  });

  test('throws ValidationError on missing required field', () => {
    const req = mockReq({});
    expect(() => validateBody(schemas.assetUpload)(req, {}, jest.fn())).toThrow(/filename/);
  });
});

describe('schemas.backupConfigUpdate', () => {
  test('accepts valid patch', () => {
    const { error, value } = schemas.backupConfigUpdate.validate({
      backups: { app1: { enabled: true, schedule: 'daily' } },
      defaultRetention: { keep: 7 },
    });
    expect(error).toBeUndefined();
    expect(value).toHaveProperty('backups');
  });

  test('strips unknown top-level keys', () => {
    const { error, value } = schemas.backupConfigUpdate.validate({
      backups: {},
      malicious: true,
    }, { stripUnknown: true });
    expect(error).toBeUndefined();
    expect(value).not.toHaveProperty('malicious');
  });

  test('rejects unknown keys inside per-app backup config', () => {
    const { error } = schemas.backupConfigUpdate.validate({
      backups: { app1: { enabled: true, schedule: 'daily', rce: 'yes' } },
    }, { stripUnknown: false });
    expect(error).toBeDefined();
  });

  test('rejects invalid retention.keep', () => {
    const { error } = schemas.backupConfigUpdate.validate({
      defaultRetention: { keep: 0 },
    });
    expect(error).toBeDefined();
    expect(error.details[0].message).toMatch(/keep/);
  });
});

describe('schemas.backupScheduleCreate', () => {
  test('requires appId', () => {
    const { error } = schemas.backupScheduleCreate.validate({});
    expect(error).toBeDefined();
  });

  test('accepts full valid body', () => {
    const { error, value } = schemas.backupScheduleCreate.validate({
      appId: 'plex',
      enabled: true,
      schedule: 'daily',
      retention: { keep: 7 },
      destination: 'local',
      maxStorageBytes: '10GB',
    });
    expect(error).toBeUndefined();
    expect(value.appId).toBe('plex');
  });

  test('rejects invalid destination', () => {
    const { error } = schemas.backupScheduleCreate.validate({
      appId: 'plex',
      destination: 'malicious-cloud',
    });
    expect(error).toBeDefined();
  });

  test('accepts numeric custom schedule (e.g. "30m", "6h")', () => {
    const { error: e1 } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: '30m' });
    expect(e1).toBeUndefined();
    const { error: e2 } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: '6h' });
    expect(e2).toBeUndefined();
  });

  test('rejects garbage schedule string', () => {
    const { error } = schemas.backupScheduleCreate.validate({ appId: 'plex', schedule: 'abc' });
    expect(error).toBeDefined();
  });
});

describe('schemas.appDeploy', () => {
  test('requires appId + config.subdomain', () => {
    const { error } = schemas.appDeploy.validate({});
    expect(error).toBeDefined();
  });

  test('accepts minimal valid deploy', () => {
    const { error, value } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex' },
    });
    expect(error).toBeUndefined();
    expect(value.config.subdomain).toBe('plex');
  });

  test('rejects port out of range', () => {
    const { error } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', port: 99999 },
    });
    expect(error).toBeDefined();
  });

  test('accepts valid IP in allowedIPs', () => {
    const { error } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['192.168.1.1', '10.0.0.0/24'] },
    });
    expect(error).toBeUndefined();
  });

  test('rejects malformed CIDR in allowedIPs', () => {
    const { error } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['999.999.999.999/99'] },
    });
    expect(error).toBeDefined();
  });

  test('rejects malformed IPv6 CIDR (regression: hex/colon regex was permissive)', () => {
    // Old regex /^[0-9a-fA-F:]+\/(\d{1,3})$/ accepted these; Joi's authoritative
    // CIDR validator must reject them.
    const { error: e1 } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['::::/64'] },
    });
    expect(e1).toBeDefined();
    const { error: e2 } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['zzzz:::/64'] },
    });
    expect(e2).toBeDefined();
    const { error: e3 } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['not-an-ip'] },
    });
    expect(e3).toBeDefined();
  });

  test('accepts valid IPv6 CIDR', () => {
    const { error } = schemas.appDeploy.validate({
      appId: 'plex',
      config: { subdomain: 'plex', allowedIPs: ['2001:db8::/32'] },
    });
    expect(error).toBeUndefined();
  });

  test('accepts valid customVolumes objects', () => {
    const { error } = schemas.appDeploy.validate({
      appId: 'plex',
      config: {
        subdomain: 'plex',
        customVolumes: [{ hostPath: '/data/movies', containerPath: '/movies' }],
      },
    });
    expect(error).toBeUndefined();
  });

  test('preserves unknown template-specific config fields (forward-compat)', () => {
    // appDeploy.config uses .unknown(true) so future template-specific fields
    // (e.g. a new app that posts `apiKey`, `databaseType`, ...) survive validation.
    const { error, value } = schemas.appDeploy.validate({
      appId: 'plex',
      config: {
        subdomain: 'plex',
        aFutureTemplateField: 'xyz',
        apiKey: 'secret',
      },
    });
    expect(error).toBeUndefined();
    expect(value.config).toHaveProperty('aFutureTemplateField', 'xyz');
    expect(value.config).toHaveProperty('apiKey', 'secret');
  });

  test('preserves template-specific config fields (sslType, dnsType, plexClaimToken)', () => {
    // The frontend posts these — they must survive validation or deployments break.
    const { error, value } = schemas.appDeploy.validate({
      appId: 'plex',
      config: {
        subdomain: 'plex',
        sslType: 'self-signed',
        dnsType: 'private',
        plexClaimToken: 'claim-abc-123',
      },
    });
    expect(error).toBeUndefined();
    expect(value.config).toHaveProperty('sslType', 'self-signed');
    expect(value.config).toHaveProperty('dnsType', 'private');
    expect(value.config).toHaveProperty('plexClaimToken', 'claim-abc-123');
  });
});

describe('schemas.assetUpload', () => {
  test('requires both fields', () => {
    const { error: e1 } = schemas.assetUpload.validate({ filename: 'logo.png' });
    expect(e1).toBeDefined();
    const { error: e2 } = schemas.assetUpload.validate({ data: 'abc' });
    expect(e2).toBeDefined();
  });

  test('accepts valid upload', () => {
    const { error } = schemas.assetUpload.validate({
      filename: 'logo.png',
      data: 'data:image/png;base64,iVBORw0KGgo=',
    });
    expect(error).toBeUndefined();
  });
});

describe('schemas.backupRestoreFile', () => {
  test('accepts empty body', () => {
    const { error } = schemas.backupRestoreFile.validate({});
    expect(error).toBeUndefined();
  });

  test('accepts optional fields', () => {
    const { error } = schemas.backupRestoreFile.validate({
      encryptionKey: 'secret',
      restartContainers: true,
    });
    expect(error).toBeUndefined();
  });

  test('strips unknown keys', () => {
    const { error, value } = schemas.backupRestoreFile.validate({
      encryptionKey: 'secret',
      malicious: 'yes',
    }, { stripUnknown: true });
    expect(error).toBeUndefined();
    expect(value).not.toHaveProperty('malicious');
  });
});

describe('schemas.backupRestore', () => {
  test('accepts empty body (all fields optional)', () => {
    const { error, value } = schemas.backupRestore.validate({});
    expect(error).toBeUndefined();
    expect(value).toEqual({});
  });

  test('accepts known control flags', () => {
    const { error } = schemas.backupRestore.validate({
      encryptionKey: 'secret',
      restartContainers: true,
      services: true,
      config: true,
      credentials: true,
      volumes: true,
    });
    expect(error).toBeUndefined();
  });

  test('strips unknown keys', () => {
    const { error, value } = schemas.backupRestore.validate({
      services: true,
      shellCommand: 'rm -rf /',
    }, { stripUnknown: true });
    expect(error).toBeUndefined();
    expect(value).not.toHaveProperty('shellCommand');
  });
});

describe('schemas.appRestore', () => {
  test('accepts empty body via middleware', () => {
    const req = mockReq({});
    const next = jest.fn();
    validateBody(schemas.appRestore)(req, {}, next);
    expect(next).toHaveBeenCalled();
  });

  test('rejects body with any key via middleware', () => {
    const req = mockReq({ filename: 'backup.tar' });
    expect(() => validateBody(schemas.appRestore)(req, {}, jest.fn())).toThrow(/empty/);
  });

  test('rejects non-object body via middleware', () => {
    const req = mockReq('just a string');
    expect(() => validateBody(schemas.appRestore)(req, {}, jest.fn())).toThrow(/empty/);
  });
});

describe('schemas.appRevert', () => {
  test('accepts empty body', () => {
    const { error } = schemas.appRevert.validate({});
    expect(error).toBeUndefined();
  });

  test('accepts optional encryption key + restart flag', () => {
    const { error } = schemas.appRevert.validate({
      encryptionKey: 'secret',
      restartContainers: true,
    });
    expect(error).toBeUndefined();
  });

  test('strips unknown keys (no shell injection vector)', () => {
    const { error, value } = schemas.appRevert.validate({
      encryptionKey: 'secret',
      path: '/etc/passwd',
      shellCommand: 'rm -rf /',
    }, { stripUnknown: true });
    expect(error).toBeUndefined();
    expect(value).not.toHaveProperty('path');
    expect(value).not.toHaveProperty('shellCommand');
  });
});

describe('schemas.logoUpload', () => {
  test('requires at least one field', () => {
    const { error } = schemas.logoUpload.validate({});
    expect(error).toBeDefined();
  });

  test('accepts single data field', () => {
    const { error } = schemas.logoUpload.validate({ data: 'data:image/png;base64,abc' });
    expect(error).toBeUndefined();
  });

  test('accepts dataDark + dataLight pair', () => {
    const { error } = schemas.logoUpload.validate({
      dataDark: 'data:image/png;base64,dark',
      dataLight: 'data:image/png;base64,light',
    });
    expect(error).toBeUndefined();
  });

  test('accepts position enum', () => {
    const { error } = schemas.logoUpload.validate({
      data: 'data:image/png;base64,abc',
      position: 'center',
    });
    expect(error).toBeUndefined();
  });

  test('rejects invalid position', () => {
    const { error } = schemas.logoUpload.validate({
      data: 'data:image/png;base64,abc',
      position: 'diagonal',
    });
    expect(error).toBeDefined();
  });

  test('rejects empty-string data fields', () => {
    const { error } = schemas.logoUpload.validate({ data: '' });
    expect(error).toBeDefined();
  });
});
