import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { initKtxProject, type KtxLocalProject } from '../../../../../src/context/project/project.js';
import { WarehouseCatalogService } from '../../../../../src/context/scan/warehouse-catalog.js';
import type { ToolContext } from '../../../../../src/context/tools/base-tool.js';
import { EntityDetailsTool } from '../../../../../src/context/ingest/tools/warehouse-verification/entity-details.tool.js';

describe('EntityDetailsTool', () => {
  let tempDir: string;
  let project: KtxLocalProject;
  let tool: EntityDetailsTool;
  let context: ToolContext;

  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), 'ktx-entity-details-'));
    project = await initKtxProject({ projectDir: join(tempDir, 'project') });
    await seedLiveDatabaseScan();
    tool = new EntityDetailsTool(() => new WarehouseCatalogService({ fileStore: project.fileStore }));
    context = {
      sourceId: 'ingest',
      messageId: 'm1',
      userId: 'system',
      session: {
        allowedConnectionNames: new Set(['warehouse']),
      } as any,
    };
  });

  afterEach(async () => {
    await rm(tempDir, { recursive: true, force: true });
  });

  async function seedLiveDatabaseScan(connectionId = 'warehouse', syncId = 'sync-1') {
    const root = `raw-sources/${connectionId}/live-database/${syncId}`;
    await project.fileStore.writeFile(
      `${root}/connection.json`,
      JSON.stringify({ connectionId, driver: 'postgres', extractedAt: '2026-05-12T00:00:00.000Z' }, null, 2),
      'ktx',
      'ktx@example.com',
      'seed connection',
    );
    await project.fileStore.writeFile(
      `${root}/tables/orders.json`,
      JSON.stringify(
        {
          catalog: null,
          db: 'public',
          name: 'orders',
          kind: 'table',
          comment: 'Customer orders',
          estimatedRows: 12,
          columns: [
            {
              name: 'id',
              nativeType: 'integer',
              normalizedType: 'integer',
              dimensionType: 'number',
              nullable: false,
              primaryKey: true,
              comment: 'Order id',
            },
            {
              name: 'status',
              nativeType: 'text',
              normalizedType: 'text',
              dimensionType: 'string',
              nullable: false,
              primaryKey: false,
              comment: 'Order status',
            },
          ],
          foreignKeys: [],
        },
        null,
        2,
      ),
      'ktx',
      'ktx@example.com',
      'seed orders',
    );
    await project.fileStore.writeFile(
      `${root}/enrichment/relationship-profile.json`,
      JSON.stringify(
        {
          connectionId,
          driver: 'postgres',
          tables: [{ table: { catalog: null, db: 'public', name: 'orders' }, rowCount: 12 }],
          columns: {
            'orders.status': {
              table: { catalog: null, db: 'public', name: 'orders' },
              column: 'status',
              rowCount: 12,
              nullCount: 0,
              distinctCount: 2,
              nullRate: 0,
              sampleValues: ['paid', 'refunded'],
            },
          },
        },
        null,
        2,
      ),
      'ktx',
      'ktx@example.com',
      'seed profile',
    );
  }

  it('returns scoped table detail for a display target', async () => {
    const result = await tool.call({ connectionId: 'warehouse', targets: [{ display: 'public.orders' }] }, context);

    expect(result.markdown).toContain('### public.orders');
    expect(result.markdown).toContain('- status (text, nullable=false)');
    expect(result.markdown).toContain('sample: ["paid","refunded"]');
    expect(result.structured.scanAvailable).toBe(true);
    expect(result.structured.resolved).toHaveLength(1);
  });

  it('resolves display targets that include a column name', async () => {
    const result = await tool.call(
      { connectionId: 'warehouse', targets: [{ display: 'public.orders.status' }] },
      context,
    );

    expect(result.markdown).toContain('### public.orders');
    expect(result.markdown).toContain('- status (text, nullable=false)');
    expect(result.markdown).not.toContain('- id (integer');
    expect(result.structured.resolved).toHaveLength(1);
    expect(result.structured.resolved[0]?.columns.map((column) => column.name)).toEqual(['status']);
  });

  it('reports missing explicit columns instead of returning an empty column list', async () => {
    const result = await tool.call(
      { connectionId: 'warehouse', targets: [{ display: 'public.orders.plan_tier' }] },
      context,
    );

    expect(result.markdown).toContain('Column not found in scan: public.orders.plan_tier');
    expect(result.markdown).toContain('Available columns: id, status');
    expect(result.structured.resolved).toHaveLength(0);
    expect(result.structured.missing).toHaveLength(1);
  });

  it('reports missing structured table targets in model-visible markdown', async () => {
    const result = await tool.call(
      {
        connectionId: 'warehouse',
        targets: [{ catalog: null, db: 'public', name: 'orderz' }],
      },
      context,
    );

    expect(result.markdown).toContain('Not found in scan: public.orderz');
    expect(result.markdown).toContain('Closest matches: orders');
    expect(result.structured.resolved).toHaveLength(0);
    expect(result.structured.missing).toHaveLength(1);
  });

  it('reports missing structured column targets in model-visible markdown', async () => {
    const result = await tool.call(
      {
        connectionId: 'warehouse',
        targets: [{ catalog: null, db: 'public', name: 'orders', column: 'plan_tier' }],
      },
      context,
    );

    expect(result.markdown).toContain('Column not found in scan: public.orders.plan_tier');
    expect(result.markdown).toContain('Available columns: id, status');
    expect(result.structured.resolved).toHaveLength(0);
    expect(result.structured.missing).toHaveLength(1);
  });

  it('returns a no-scan state distinct from not found', async () => {
    const result = await tool.call(
      { connectionId: 'empty', targets: [{ display: 'public.orders' }] },
      { ...context, session: { ...context.session!, allowedConnectionNames: new Set(['empty']) } },
    );

    expect(result.markdown).toContain('No live-database scan available for connection "empty"; run `ktx scan` first.');
    expect(result.structured.scanAvailable).toBe(false);
  });

  it('refuses out-of-scope connections', async () => {
    const result = await tool.call({ connectionId: 'billing', targets: [{ display: 'public.orders' }] }, context);

    expect(result.markdown).toContain('Connection "billing" is not available to this ingest stage.');
    expect(result.structured.scanAvailable).toBe(false);
  });

  it('uses connectionId as the public input field', async () => {
    const legacyConnectionField = ['connection', 'Name'].join('');

    expect(
      tool.parseInput({
        connectionId: 'warehouse',
        targets: [{ display: 'public.orders' }],
      }),
    ).toEqual({
      connectionId: 'warehouse',
      targets: [{ display: 'public.orders' }],
    });

    expect(() =>
      tool.parseInput({
        [legacyConnectionField]: 'warehouse',
        targets: [{ display: 'public.orders' }],
      }),
    ).toThrow();
  });
});
