import { describe, expect, it, vi } from 'vitest';
import type { StagedExploreFile, StagedLookmlModelsFile } from '../../../../../src/context/ingest/adapters/looker/types.js';
import {
  buildLookerPullConfigFromInputs,
  collectExploreParseItems,
  computeLookerMappingDrift,
  discoverLookerConnections,
  lookerDialectToConnectionType,
  projectParsedIdentifier,
  refreshLookerMappingPlaceholders,
  sqlglotDialectForConnectionType,
  suggestKtxConnectionForLookerConnection,
  validateLookerMappings,
  validateLookerWarehouseTarget,
} from '../../../../../src/context/ingest/adapters/looker/mapping.js';

const liveConnections = [
  {
    name: 'b2b_sandbox_bq',
    host: 'warehouse.example.com',
    database: 'analytics',
    schema: null,
    dialect: 'bigquery_standard_sql',
  },
  {
    name: 'pg_runtime',
    host: 'pg.internal:5432',
    database: 'app',
    schema: 'public',
    dialect: 'postgres',
  },
];

const mappedExplore: StagedExploreFile = {
  modelName: 'b2b',
  exploreName: 'sales_pipeline',
  label: 'Sales Pipeline',
  description: null,
  rawSqlTableName: 'proj.analytics.opportunities AS opportunities',
  connectionName: 'b2b_sandbox_bq',
  viewName: 'opportunities',
  fields: { dimensions: [], measures: [] },
  joins: [
    {
      name: 'accounts',
      type: 'left_outer',
      relationship: 'many_to_one',
      rawSqlTableName: 'proj.analytics.accounts',
      sqlOn: null,
      from: null,
      targetTable: null,
    },
  ],
  targetWarehouseConnectionId: null,
  targetTable: null,
};

const models: StagedLookmlModelsFile = {
  models: [{ name: 'b2b', label: 'B2B', explores: [{ name: 'sales_pipeline', label: 'Sales Pipeline' }] }],
};

describe('discoverLookerConnections', () => {
  it('delegates to the runtime client connection discovery method', async () => {
    const client = { listLookerConnections: vi.fn().mockResolvedValue(liveConnections) };

    await expect(discoverLookerConnections(client)).resolves.toEqual(liveConnections);
    expect(client.listLookerConnections).toHaveBeenCalledTimes(1);
  });
});

describe('looker dialect and target validation helpers', () => {
  it('maps Looker dialect names to KTX connection types', () => {
    expect(lookerDialectToConnectionType('bigquery_standard_sql')).toBe('BIGQUERY');
    expect(lookerDialectToConnectionType('postgres')).toBe('POSTGRESQL');
    expect(lookerDialectToConnectionType('mssql')).toBeNull();
    expect(lookerDialectToConnectionType('tsql')).toBeNull();
    expect(lookerDialectToConnectionType('unknown')).toBeNull();
  });

  it('maps supported warehouse connection types to sqlglot dialects', () => {
    expect(sqlglotDialectForConnectionType('BIGQUERY')).toBe('bigquery');
    expect(sqlglotDialectForConnectionType('POSTGRESQL')).toBe('postgres');
    expect(sqlglotDialectForConnectionType('LOOKER')).toBeNull();
  });

  it('returns a structured failure for unsupported Looker warehouse targets', () => {
    expect(validateLookerWarehouseTarget('LOOKER')).toEqual({
      ok: false,
      reason: 'Connection type LOOKER cannot be used as a Looker warehouse mapping target',
    });
  });
});

describe('suggestKtxConnectionForLookerConnection', () => {
  it('returns the single deterministic target with matching type, host, and database', () => {
    expect(
      suggestKtxConnectionForLookerConnection({
        lookerConnection: liveConnections[1],
        candidateConnections: [
          {
            id: 'wrong-type',
            connection_type: 'MYSQL',
            connection_params: { host: 'pg.internal', database: 'app' },
          },
          {
            id: 'pg-target',
            connection_type: 'POSTGRESQL',
            connection_params: { host: 'PG.INTERNAL', database: 'APP' },
          },
        ],
      }),
    ).toBe('pg-target');
  });

  it('returns null when more than one target matches', () => {
    expect(
      suggestKtxConnectionForLookerConnection({
        lookerConnection: liveConnections[1],
        candidateConnections: [
          {
            id: 'first',
            connection_type: 'POSTGRESQL',
            connection_params: { host: 'pg.internal', database: 'app' },
          },
          {
            id: 'second',
            connection_type: 'POSTGRESQL',
            connection_params: { host: 'pg.internal:5432', database: 'APP' },
          },
        ],
      }),
    ).toBeNull();
  });
});

describe('refreshLookerMappingPlaceholders', () => {
  it('adds newly discovered placeholders and refreshes live metadata without dropping saved targets', () => {
    expect(
      refreshLookerMappingPlaceholders({
        stored: [
          {
            lookerConnectionName: 'b2b_sandbox_bq',
            ktxConnectionId: 'warehouse',
            lookerHost: null,
            lookerDatabase: null,
            lookerDialect: null,
          },
        ],
        live: liveConnections,
      }),
    ).toEqual({
      changed: true,
      mappings: [
        {
          lookerConnectionName: 'b2b_sandbox_bq',
          ktxConnectionId: 'warehouse',
          lookerHost: 'warehouse.example.com',
          lookerDatabase: 'analytics',
          lookerDialect: 'bigquery_standard_sql',
        },
        {
          lookerConnectionName: 'pg_runtime',
          ktxConnectionId: null,
          lookerHost: 'pg.internal:5432',
          lookerDatabase: 'app',
          lookerDialect: 'postgres',
        },
      ],
    });
  });
});

describe('computeLookerMappingDrift and validateLookerMappings', () => {
  it('reports unmapped live connections, stale stored mappings, and in-sync mappings', () => {
    expect(
      computeLookerMappingDrift({
        storedMappings: [
          {
            lookerConnectionName: 'b2b_sandbox_bq',
            ktxConnectionId: 'warehouse',
            lookerHost: null,
            lookerDatabase: null,
            lookerDialect: null,
          },
          {
            lookerConnectionName: 'stale_runtime',
            ktxConnectionId: 'warehouse',
            lookerHost: null,
            lookerDatabase: null,
            lookerDialect: null,
          },
        ],
        discovered: liveConnections,
      }),
    ).toEqual({
      unmappedDiscovered: [liveConnections[1]],
      staleMappings: [{ lookerConnectionName: 'stale_runtime', reason: 'looker_connection_not_found' }],
      inSync: [{ lookerConnectionName: 'b2b_sandbox_bq', ktxConnectionId: 'warehouse' }],
    });
  });

  it('validates missing and unsupported target connection ids', () => {
    expect(
      validateLookerMappings({
        mappings: [
          {
            lookerConnectionName: 'b2b_sandbox_bq',
            ktxConnectionId: 'missing',
            lookerHost: null,
            lookerDatabase: null,
            lookerDialect: null,
          },
          {
            lookerConnectionName: 'pg_runtime',
            ktxConnectionId: 'looker-target',
            lookerHost: null,
            lookerDatabase: null,
            lookerDialect: null,
          },
        ],
        knownKtxConnectionIds: new Set(['looker-target']),
        knownConnectionTypes: new Map([['looker-target', 'LOOKER']]),
      }),
    ).toEqual({
      ok: false,
      errors: [
        { key: 'b2b_sandbox_bq', reason: 'KTX connection missing does not exist' },
        {
          key: 'pg_runtime',
          reason: 'Connection type LOOKER cannot be used as a Looker warehouse mapping target',
        },
      ],
    });
  });
});

describe('collectExploreParseItems and projectParsedIdentifier', () => {
  it('collects base explore and join parser inputs for mapped explores', () => {
    expect(
      collectExploreParseItems({
        explore: mappedExplore,
        connectionMappings: { b2b_sandbox_bq: 'warehouse' },
        targetConnections: new Map([['warehouse', { id: 'warehouse', connection_type: 'BIGQUERY' }]]),
      }),
    ).toEqual({
      parsedTargetTables: {},
      parseItems: [
        {
          key: 'b2b.sales_pipeline',
          sql_table_name: 'proj.analytics.opportunities AS opportunities',
          dialect: 'bigquery',
        },
        {
          key: 'b2b.sales_pipeline.accounts',
          sql_table_name: 'proj.analytics.accounts',
          dialect: 'bigquery',
        },
      ],
    });
  });

  it('projects successful and failed parser rows into KTX parsed target tables', () => {
    expect(
      projectParsedIdentifier({
        ok: true,
        catalog: 'proj',
        schema: 'analytics',
        name: 'accounts',
        canonical_table: 'proj.analytics.accounts',
      }),
    ).toEqual({
      ok: true,
      catalog: 'proj',
      schema: 'analytics',
      name: 'accounts',
      canonicalTable: 'proj.analytics.accounts',
    });

    expect(projectParsedIdentifier({ ok: false, reason: 'derived_table_not_supported' })).toEqual({
      ok: false,
      reason: 'derived_table_not_supported',
    });
  });
});

describe('buildLookerPullConfigFromInputs', () => {
  it('builds the hosted-equivalent Looker pull config from caller-loaded inputs', async () => {
    const parser = {
      parse: vi.fn().mockResolvedValue({
        'b2b.sales_pipeline': {
          ok: true,
          catalog: 'proj',
          schema: 'analytics',
          name: 'opportunities',
          canonical_table: 'proj.analytics.opportunities',
        },
        'b2b.sales_pipeline.accounts': {
          ok: true,
          catalog: 'proj',
          schema: 'analytics',
          name: 'accounts',
          canonical_table: 'proj.analytics.accounts',
        },
      }),
    };
    const client = {
      listLookmlModels: vi.fn().mockResolvedValue(models),
      getExplore: vi.fn().mockResolvedValue(mappedExplore),
    };

    await expect(
      buildLookerPullConfigFromInputs({
        lookerConnectionId: 'prod-looker',
        cursors: {
          dashboardsLastSyncedAt: '2026-05-01T00:00:00.000Z',
          looksLastSyncedAt: null,
        },
        refreshedMappings: [
          {
            lookerConnectionName: 'b2b_sandbox_bq',
            ktxConnectionId: 'warehouse',
            lookerHost: 'warehouse.example.com',
            lookerDatabase: 'analytics',
            lookerDialect: 'bigquery_standard_sql',
          },
        ],
        targetConnections: new Map([['warehouse', { id: 'warehouse', connection_type: 'BIGQUERY' }]]),
        client,
        parser,
      }),
    ).resolves.toEqual({
      lookerConnectionId: 'prod-looker',
      dashboardUpdatedSince: '2026-05-01T00:00:00.000Z',
      lookUpdatedSince: null,
      connectionMappings: { b2b_sandbox_bq: 'warehouse' },
      connectionTypes: { b2b_sandbox_bq: 'BIGQUERY' },
      parsedTargetTables: {
        'b2b.sales_pipeline': {
          ok: true,
          catalog: 'proj',
          schema: 'analytics',
          name: 'opportunities',
          canonicalTable: 'proj.analytics.opportunities',
        },
        'b2b.sales_pipeline.accounts': {
          ok: true,
          catalog: 'proj',
          schema: 'analytics',
          name: 'accounts',
          canonicalTable: 'proj.analytics.accounts',
        },
      },
    });
  });

  it('marks parser failures as parse_error without blocking pull-config construction', async () => {
    const parser = { parse: vi.fn().mockRejectedValue(new Error('python unavailable')) };
    const client = {
      listLookmlModels: vi.fn().mockResolvedValue(models),
      getExplore: vi.fn().mockResolvedValue(mappedExplore),
    };

    const config = await buildLookerPullConfigFromInputs({
      lookerConnectionId: 'prod-looker',
      cursors: { dashboardsLastSyncedAt: null, looksLastSyncedAt: null },
      refreshedMappings: [
        {
          lookerConnectionName: 'b2b_sandbox_bq',
          ktxConnectionId: 'warehouse',
          lookerHost: null,
          lookerDatabase: null,
          lookerDialect: null,
        },
      ],
      targetConnections: new Map([['warehouse', { id: 'warehouse', connection_type: 'BIGQUERY' }]]),
      client,
      parser,
    });

    expect(config.parsedTargetTables).toMatchObject({
      'b2b.sales_pipeline': { ok: false, reason: 'parse_error' },
      'b2b.sales_pipeline.accounts': { ok: false, reason: 'parse_error' },
    });
  });
});
