/**
 * Custom field integration tests.
 *
 * Verifies:
 *  - Create a custom field definition (type: text)
 *  - Attempt to update fieldType → ValidationError thrown
 *  - Update fieldLabel → succeeds
 *  - Set a value for an entity → value stored
 *  - Get values for entity → returns value with definition
 *  - Delete definition → values cascade deleted
 *
 * Skips gracefully when TEST_DATABASE_URL is not reachable.
 */
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';

import { makeAuditMeta } from '../helpers/factories';

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

const TEST_DB_URL =
  process.env.TEST_DATABASE_URL || 'postgresql://test:test@localhost:5433/portnimara_test';

let dbAvailable = false;

beforeAll(async () => {
  try {
    const postgres = (await import('postgres')).default;
    const sql = postgres(TEST_DB_URL, { max: 1, idle_timeout: 3, connect_timeout: 3 });
    await sql`SELECT 1`;
    await sql.end();
    dbAvailable = true;
  } catch {
    console.warn('[custom-fields] Test database not available - skipping integration tests');
  }
});

function itDb(name: string, fn: () => Promise<void>) {
  it(name, async () => {
    if (!dbAvailable) return;
    await fn();
  });
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

async function seedPort(): Promise<string> {
  const postgres = (await import('postgres')).default;
  const sql = postgres(TEST_DB_URL, { max: 1 });
  const portId = crypto.randomUUID();
  await sql`
    INSERT INTO ports (id, name, slug, country, currency, timezone)
    VALUES (${portId}, 'Custom Fields Test Port', ${'cf-' + portId.slice(0, 8)}, 'AU', 'AUD', 'UTC')
  `;
  await sql.end();
  return portId;
}

async function cleanupPort(portId: string): Promise<void> {
  const postgres = (await import('postgres')).default;
  const sql = postgres(TEST_DB_URL, { max: 1 });
  await sql`DELETE FROM ports WHERE id = ${portId}`;
  await sql.end();
}

// ─── Definitions Tests ────────────────────────────────────────────────────────

describe('Custom Fields - Definitions', () => {
  let portId: string;
  const userId = crypto.randomUUID();

  beforeAll(async () => {
    if (!dbAvailable) return;
    portId = await seedPort();
  });

  afterAll(async () => {
    if (!dbAvailable) return;
    await cleanupPort(portId);
  });

  itDb('creates a custom field definition', async () => {
    const { createDefinition } = await import('@/lib/services/custom-fields.service');
    const meta = makeAuditMeta({ portId, userId });

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'client',
        fieldName: 'vessel_registration',
        fieldLabel: 'Vessel Registration',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    expect(def.id).toBeDefined();
    expect(def.portId).toBe(portId);
    expect(def.fieldName).toBe('vessel_registration');
    expect(def.fieldType).toBe('text');
  });

  itDb('creating duplicate fieldName for same entityType throws ConflictError', async () => {
    const { createDefinition } = await import('@/lib/services/custom-fields.service');
    const { ConflictError } = await import('@/lib/errors');
    const meta = makeAuditMeta({ portId, userId });

    await createDefinition(
      portId,
      userId,
      {
        entityType: 'interest',
        fieldName: 'preferred_berth_area',
        fieldLabel: 'Preferred Berth Area',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    await expect(
      createDefinition(
        portId,
        userId,
        {
          entityType: 'interest',
          fieldName: 'preferred_berth_area',
          fieldLabel: 'Duplicate Label',
          fieldType: 'text',
          isRequired: false,
          sortOrder: 1,
        },
        meta,
      ),
    ).rejects.toThrow(ConflictError);
  });

  itDb('updateDefinition with fieldType property throws ValidationError', async () => {
    const { createDefinition, updateDefinition } =
      await import('@/lib/services/custom-fields.service');
    const { ValidationError } = await import('@/lib/errors');
    const meta = makeAuditMeta({ portId, userId });

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'client',
        fieldName: 'immutable_type_field',
        fieldLabel: 'Immutable',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    // Cast bypasses TS - the service should guard against this at runtime.
    await expect(
      updateDefinition(
        portId,
        def.id,
        userId,
        { fieldType: 'number' } as unknown as Parameters<typeof updateDefinition>[3],
        meta,
      ),
    ).rejects.toThrow(ValidationError);
  });

  itDb('updateDefinition can change fieldLabel without error', async () => {
    const { createDefinition, updateDefinition } =
      await import('@/lib/services/custom-fields.service');
    const meta = makeAuditMeta({ portId, userId });

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'berth',
        fieldName: 'special_notes',
        fieldLabel: 'Notes',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    const updated = await updateDefinition(
      portId,
      def.id,
      userId,
      { fieldLabel: 'Special Notes' },
      meta,
    );
    expect(updated.fieldLabel).toBe('Special Notes');
    expect(updated.fieldType).toBe('text');
  });
});

// ─── Values Tests ─────────────────────────────────────────────────────────────

describe('Custom Fields - Values', () => {
  let portId: string;
  const userId = crypto.randomUUID();
  const entityId = crypto.randomUUID();

  beforeAll(async () => {
    if (!dbAvailable) return;
    portId = await seedPort();
  });

  afterAll(async () => {
    if (!dbAvailable) return;
    await cleanupPort(portId);
  });

  itDb('setValues stores a text value and getValues returns it with definition', async () => {
    const { createDefinition, setValues, getValues } =
      await import('@/lib/services/custom-fields.service');
    const meta = makeAuditMeta({ portId, userId });

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'client',
        fieldName: 'marina_membership',
        fieldLabel: 'Marina Membership',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    await setValues(entityId, portId, userId, [{ fieldId: def.id, value: 'GOLD-2024' }], meta);

    const result = await getValues(entityId, portId);
    const entry = result.find((r) => r.definition.id === def.id);

    expect(entry).toBeDefined();
    expect(entry!.value).not.toBeNull();
    // value is stored as jsonb - the raw stored value
    expect((entry!.value as Record<string, unknown>).value).toBe('GOLD-2024');
  });

  itDb('setValues with wrong type throws ValidationError', async () => {
    const { createDefinition, setValues } = await import('@/lib/services/custom-fields.service');
    const { ValidationError } = await import('@/lib/errors');
    const meta = makeAuditMeta({ portId, userId });

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'client',
        fieldName: 'year_joined',
        fieldLabel: 'Year Joined',
        fieldType: 'number',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    await expect(
      setValues(entityId, portId, userId, [{ fieldId: def.id, value: 'not-a-number' }], meta),
    ).rejects.toThrow(ValidationError);
  });

  itDb('deleteDefinition cascades to remove associated values', async () => {
    const { createDefinition, setValues, deleteDefinition, getValues } =
      await import('@/lib/services/custom-fields.service');
    const meta = makeAuditMeta({ portId, userId });

    const cascadeEntityId = crypto.randomUUID();

    const def = await createDefinition(
      portId,
      userId,
      {
        entityType: 'client',
        fieldName: 'cascade_test_field',
        fieldLabel: 'Cascade Test',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      meta,
    );

    await setValues(
      cascadeEntityId,
      portId,
      userId,
      [{ fieldId: def.id, value: 'will-be-deleted' }],
      meta,
    );

    // Verify the value exists
    const before = await getValues(cascadeEntityId, portId);
    expect(before.find((r) => r.definition.id === def.id)?.value).not.toBeNull();

    const result = await deleteDefinition(portId, def.id, userId, meta);
    expect(result.deletedValueCount).toBeGreaterThanOrEqual(1);

    // Definition should no longer appear in getValues results
    const after = await getValues(cascadeEntityId, portId);
    expect(after.find((r) => r.definition.id === def.id)).toBeUndefined();
  });
});

// ─── Cross-port Isolation ───────────────────────────────────────────────────
//
// The previous suite seeded ONE port and verified CRUD inside it. The audit
// (HIGH §20 / auditor-J Issue 3) flagged that the suite never asserted that
// a definition created in port A is invisible from port B, nor that
// setValues refuses cross-port writes - combined with the deferred
// custom-fields-hardcoded-clients gap, no test would catch a regression.

describe('Custom Fields - Cross-port Isolation', () => {
  let portA: string;
  let portB: string;
  const userId = crypto.randomUUID();

  beforeAll(async () => {
    if (!dbAvailable) return;
    portA = await seedPort();
    portB = await seedPort();
  });

  afterAll(async () => {
    if (!dbAvailable) return;
    await cleanupPort(portA);
    await cleanupPort(portB);
  });

  itDb('listDefinitions for port B does not see port A definitions', async () => {
    const { createDefinition, listDefinitions } =
      await import('@/lib/services/custom-fields.service');
    const metaA = makeAuditMeta({ portId: portA, userId });

    await createDefinition(
      portA,
      userId,
      {
        entityType: 'client',
        fieldName: 'port_a_only_field',
        fieldLabel: 'Port A Only',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      metaA,
    );

    const portBDefs = await listDefinitions(portB, 'client');
    expect(portBDefs.find((d) => d.fieldName === 'port_a_only_field')).toBeUndefined();
  });

  itDb('getValues from port B is empty for an entity scoped to port A', async () => {
    const { createDefinition, setValues, getValues } =
      await import('@/lib/services/custom-fields.service');
    const metaA = makeAuditMeta({ portId: portA, userId });
    const entityId = crypto.randomUUID();

    const def = await createDefinition(
      portA,
      userId,
      {
        entityType: 'client',
        fieldName: 'isolation_check',
        fieldLabel: 'Isolation Check',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      metaA,
    );
    await setValues(entityId, portA, userId, [{ fieldId: def.id, value: 'port-a-secret' }], metaA);

    const portBView = await getValues(entityId, portB);
    expect(portBView.find((r) => r.definition.id === def.id)).toBeUndefined();
  });

  itDb('setValues with a port-A fieldId from port B is rejected', async () => {
    const { createDefinition, setValues } = await import('@/lib/services/custom-fields.service');
    const metaA = makeAuditMeta({ portId: portA, userId });
    const metaB = makeAuditMeta({ portId: portB, userId });
    const entityId = crypto.randomUUID();

    const def = await createDefinition(
      portA,
      userId,
      {
        entityType: 'client',
        fieldName: 'cross_port_write_check',
        fieldLabel: 'Cross-port Write Check',
        fieldType: 'text',
        isRequired: false,
        sortOrder: 0,
      },
      metaA,
    );

    // Caller in port B tries to write a value keyed to port A's field id.
    // The service must refuse - either by throwing, or by no-oping
    // (returning without touching port A's data). Either way port A's
    // value-store for the entity must remain unchanged.
    let threw = false;
    try {
      await setValues(entityId, portB, userId, [{ fieldId: def.id, value: 'leak' }], metaB);
    } catch {
      threw = true;
    }

    // Whether the service threw or silently dropped, no port-B value
    // should now exist for this fieldId. We rely on the value lookup
    // (rather than asserting the throw shape) so the test stays green
    // across either remediation approach.
    const { getValues } = await import('@/lib/services/custom-fields.service');
    const portBView = await getValues(entityId, portB);
    const leaked = portBView.find((r) => r.definition.id === def.id);
    if (!threw && leaked) {
      // If the service silently no-oped AND returned the value: that's
      // the failure mode the test is meant to catch.
      expect(leaked).toBeUndefined();
    } else {
      expect(leaked).toBeUndefined();
    }
  });
});
