/**
 * Tests for validateCustomFieldValue - the private validation helper in
 * custom-fields.service.ts. Since it is not exported we test it via the
 * public setValues function, using vi.mock to avoid database calls.
 * All assertions focus on what error message (if any) is thrown.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';

// ─── Mock database + dependencies ────────────────────────────────────────────

vi.mock('@/lib/db', () => ({
  db: {
    query: {
      customFieldDefinitions: { findMany: vi.fn(), findFirst: vi.fn() },
      // Entity-port-scope checks added by the security fix; default to a
      // truthy row so existing assertions still focus on validation logic.
      clients: { findFirst: vi.fn().mockResolvedValue({ id: 'entity-1', portId: 'port-1' }) },
      interests: { findFirst: vi.fn().mockResolvedValue({ id: 'entity-1', portId: 'port-1' }) },
      berths: { findFirst: vi.fn().mockResolvedValue({ id: 'entity-1', portId: 'port-1' }) },
      yachts: { findFirst: vi.fn().mockResolvedValue({ id: 'entity-1', portId: 'port-1' }) },
      companies: { findFirst: vi.fn().mockResolvedValue({ id: 'entity-1', portId: 'port-1' }) },
    },
    insert: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
    select: vi.fn(),
  },
}));

vi.mock('@/lib/db/schema/clients', () => ({ clients: {} }));
vi.mock('@/lib/db/schema/interests', () => ({ interests: {} }));
vi.mock('@/lib/db/schema/berths', () => ({ berths: {} }));
vi.mock('@/lib/db/schema/yachts', () => ({ yachts: {} }));
vi.mock('@/lib/db/schema/companies', () => ({ companies: {} }));

vi.mock('@/lib/audit', () => ({
  createAuditLog: vi.fn().mockResolvedValue(undefined),
}));

vi.mock('@/lib/logger', () => ({
  logger: { warn: vi.fn(), error: vi.fn() },
}));

vi.mock('@/lib/db/schema/system', () => ({
  customFieldDefinitions: {},
  customFieldValues: {},
}));

// next/server is not available in vitest node environment
vi.mock('next/server', () => ({
  NextResponse: {
    json: vi.fn(),
  },
}));

import { setValues } from '@/lib/services/custom-fields.service';
import { db } from '@/lib/db';
import { ValidationError } from '@/lib/errors';

// ─── Helper to build a minimal CustomFieldDefinition ─────────────────────────

function makeDefinition(
  fieldType: string,
  extras: { isRequired?: boolean; selectOptions?: string[] } = {},
) {
  return {
    id: 'field-1',
    portId: 'port-1',
    entityType: 'client',
    fieldName: 'test_field',
    fieldLabel: 'Test Field',
    fieldType,
    selectOptions: extras.selectOptions ?? null,
    isRequired: extras.isRequired ?? false,
    sortOrder: 0,
    createdAt: new Date(),
  };
}

const AUDIT_META = {
  userId: 'user-1',
  portId: 'port-1',
  ipAddress: '127.0.0.1',
  userAgent: 'test',
};

beforeEach(() => {
  vi.clearAllMocks();

  // Default: no existing values, upsert succeeds
  const insertChain = {
    values: vi.fn().mockReturnThis(),
    onConflictDoUpdate: vi.fn().mockReturnThis(),
    returning: vi.fn().mockResolvedValue([{ id: 'cfv-1' }]),
  };
  (db.insert as ReturnType<typeof vi.fn>).mockReturnValue(insertChain);
});

/** Convenience: call setValues with a single field/value pair. */
async function validate(
  fieldType: string,
  value: unknown,
  extras?: { isRequired?: boolean; selectOptions?: string[] },
) {
  (db.query.customFieldDefinitions.findMany as ReturnType<typeof vi.fn>).mockResolvedValue([
    makeDefinition(fieldType, extras),
  ]);

  return setValues('entity-1', 'port-1', 'user-1', [{ fieldId: 'field-1', value }], AUDIT_META);
}

// ─── text ─────────────────────────────────────────────────────────────────────

describe('custom field validation - text', () => {
  it('accepts a string value', async () => {
    await expect(validate('text', 'hello')).resolves.toBeDefined();
  });

  it('rejects a number value', async () => {
    await expect(validate('text', 42)).rejects.toBeInstanceOf(ValidationError);
  });

  it('rejects a boolean value', async () => {
    await expect(validate('text', true)).rejects.toBeInstanceOf(ValidationError);
  });

  it('rejects a string longer than 1000 chars', async () => {
    await expect(validate('text', 'x'.repeat(1001))).rejects.toBeInstanceOf(ValidationError);
  });
});

// ─── number ──────────────────────────────────────────────────────────────────

describe('custom field validation - number', () => {
  it('accepts a valid number', async () => {
    await expect(validate('number', 42)).resolves.toBeDefined();
  });

  it('accepts zero', async () => {
    await expect(validate('number', 0)).resolves.toBeDefined();
  });

  it('rejects a string', async () => {
    await expect(validate('number', '42')).rejects.toBeInstanceOf(ValidationError);
  });

  it('rejects NaN', async () => {
    await expect(validate('number', NaN)).rejects.toBeInstanceOf(ValidationError);
  });
});

// ─── date ─────────────────────────────────────────────────────────────────────

describe('custom field validation - date', () => {
  it('accepts a valid ISO date string', async () => {
    await expect(validate('date', '2026-06-15')).resolves.toBeDefined();
  });

  it('accepts a full ISO datetime string', async () => {
    await expect(validate('date', '2026-06-15T10:00:00.000Z')).resolves.toBeDefined();
  });

  it('rejects "not-a-date"', async () => {
    await expect(validate('date', 'not-a-date')).rejects.toBeInstanceOf(ValidationError);
  });

  it('rejects a number', async () => {
    await expect(validate('date', 20260615)).rejects.toBeInstanceOf(ValidationError);
  });
});

// ─── boolean ─────────────────────────────────────────────────────────────────

describe('custom field validation - boolean', () => {
  it('accepts true', async () => {
    await expect(validate('boolean', true)).resolves.toBeDefined();
  });

  it('accepts false', async () => {
    await expect(validate('boolean', false)).resolves.toBeDefined();
  });

  it('rejects the string "true"', async () => {
    await expect(validate('boolean', 'true')).rejects.toBeInstanceOf(ValidationError);
  });

  it('rejects 1 (number)', async () => {
    await expect(validate('boolean', 1)).rejects.toBeInstanceOf(ValidationError);
  });
});

// ─── select ──────────────────────────────────────────────────────────────────

describe('custom field validation - select', () => {
  const options = ['Small', 'Medium', 'Large'];

  it('accepts a valid option', async () => {
    await expect(validate('select', 'Small', { selectOptions: options })).resolves.toBeDefined();
  });

  it('rejects an option not in the list', async () => {
    await expect(validate('select', 'XL', { selectOptions: options })).rejects.toBeInstanceOf(
      ValidationError,
    );
  });

  it('error message lists the valid options', async () => {
    try {
      await validate('select', 'XL', { selectOptions: options });
      expect.fail('Should have thrown');
    } catch (err) {
      expect(err).toBeInstanceOf(ValidationError);
      // The service wraps the error in ValidationError with an errors array
      const ve = err as ValidationError;
      const messages = JSON.stringify(ve);
      expect(messages).toMatch(/Small|Medium|Large/);
    }
  });
});

// ─── required / non-required null handling ───────────────────────────────────

describe('custom field validation - required vs optional null', () => {
  it('required field: null value → throws ValidationError', async () => {
    await expect(validate('text', null, { isRequired: true })).rejects.toBeInstanceOf(
      ValidationError,
    );
  });

  it('required field: undefined value → throws ValidationError', async () => {
    await expect(validate('text', undefined, { isRequired: true })).rejects.toBeInstanceOf(
      ValidationError,
    );
  });

  it('non-required field: null value → succeeds (no error)', async () => {
    // null for non-required means "clear the value" - setValues will upsert null
    await expect(validate('text', null, { isRequired: false })).resolves.toBeDefined();
  });
});
