import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Database from 'better-sqlite3';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js';
import type { KtxLocalProject } from '../src/context/project/project.js';
import {
  buildLocalStatsStatus,
  buildProjectStatus,
  renderProjectStatus,
} from '../src/status-project.js';

function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
  return {
    projectDir: '/work/proj',
    configPath: '/work/proj/ktx.yaml',
    config,
    coreConfig: {} as KtxLocalProject['coreConfig'],
    git: {} as KtxLocalProject['git'],
    fileStore: {} as KtxLocalProject['fileStore'],
  };
}

function withEmbeddings(
  config: KtxProjectConfig,
  embeddings: KtxProjectConfig['ingest']['embeddings'],
): KtxProjectConfig {
  return {
    ...config,
    ingest: { ...config.ingest, embeddings },
    scan: { ...config.scan, enrichment: { ...config.scan.enrichment, embeddings } },
  };
}

function withClaudeCodeLlm(config: KtxProjectConfig): KtxProjectConfig {
  return {
    ...config,
    llm: {
      ...config.llm,
      provider: { backend: 'claude-code' },
      models: { ...config.llm.models, default: 'sonnet' },
    },
  };
}

function baseProjectConfig(): KtxProjectConfig {
  return withClaudeCodeLlm(buildDefaultKtxProjectConfig());
}

const stubClaudeCodeAuthProbe = async () => ({ ok: true as const });

describe('buildProjectStatus embeddings', () => {
  it('reports sentence-transformers with explicit base_url as ok', async () => {
    const project = projectWithConfig(
      withEmbeddings(baseProjectConfig(), {
        backend: 'sentence-transformers',
        model: 'all-MiniLM-L6-v2',
        dimensions: 384,
        sentenceTransformers: { base_url: 'http://my-st:8080', pathPrefix: '' },
      }),
    );

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
    });

    expect(status.embeddings).toMatchObject({
      backend: 'sentence-transformers',
      status: 'ok',
      detail: 'service: http://my-st:8080',
    });
  });

  it('reports sentence-transformers with omitted base_url as managed daemon (ok)', async () => {
    const project = projectWithConfig(
      withEmbeddings(baseProjectConfig(), {
        backend: 'sentence-transformers',
        model: 'all-MiniLM-L6-v2',
        dimensions: 384,
      }),
    );

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
    });

    expect(status.embeddings).toMatchObject({
      backend: 'sentence-transformers',
      status: 'ok',
      detail: 'managed local embeddings daemon',
    });
    expect(status.verdictReason).not.toMatch(/embedding credentials missing/);
  });

  it('reports sentence-transformers with empty base_url string as managed daemon (ok)', async () => {
    const project = projectWithConfig(
      withEmbeddings(baseProjectConfig(), {
        backend: 'sentence-transformers',
        model: 'all-MiniLM-L6-v2',
        dimensions: 384,
        sentenceTransformers: { base_url: '', pathPrefix: '' },
      }),
    );

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
    });

    expect(status.embeddings).toMatchObject({
      backend: 'sentence-transformers',
      status: 'ok',
      detail: 'managed local embeddings daemon',
    });
  });

  it('reports openai backend with missing key as warn', async () => {
    const project = projectWithConfig(
      withEmbeddings(baseProjectConfig(), {
        backend: 'openai',
        model: 'text-embedding-3-small',
        dimensions: 1536,
        openai: { api_key: 'env:OPENAI_API_KEY' }, // pragma: allowlist secret
      }),
    );

    const status = await buildProjectStatus(project, {
      env: {},
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
    });

    expect(status.embeddings.status).toBe('warn');
    expect(status.verdictReason).toMatch(/embedding credentials missing/);
  });
});

function withPostgresQueryHistory(config: KtxProjectConfig): KtxProjectConfig {
  return {
    ...config,
    connections: {
      ...config.connections,
      analytics: {
        driver: 'postgres',
        url: 'env:ANALYTICS_DATABASE_URL',
        context: { queryHistory: { enabled: true } },
      } as KtxProjectConfig['connections'][string],
    },
  };
}

function withSnowflakeQueryHistory(config: KtxProjectConfig): KtxProjectConfig {
  return {
    ...config,
    connections: {
      ...config.connections,
      warehouse: {
        driver: 'snowflake',
        account: 'EMOVRJS-CZ07756',
        warehouse: 'COMPUTE_WH',
        database: 'ANALYTICS',
        username: 'svc_ktx',
        password: 'env:SNOWFLAKE_PASSWORD', // pragma: allowlist secret
        context: { queryHistory: { enabled: true } },
      } as KtxProjectConfig['connections'][string],
    },
  };
}

function withBigQueryQueryHistory(config: KtxProjectConfig): KtxProjectConfig {
  return {
    ...config,
    connections: {
      ...config.connections,
      bq: {
        driver: 'bigquery',
        credentials_json: 'env:BQ_CREDENTIALS_JSON',
        context: { queryHistory: { enabled: true } },
      } as KtxProjectConfig['connections'][string],
    },
  };
}

function withMysqlQueryHistory(config: KtxProjectConfig): KtxProjectConfig {
  return {
    ...config,
    connections: {
      ...config.connections,
      legacy: {
        driver: 'mysql',
        host: 'db.example.com',
        database: 'analytics',
        username: 'svc',
        password: 'env:MYSQL_PASSWORD', // pragma: allowlist secret
        context: { queryHistory: { enabled: true } },
      } as KtxProjectConfig['connections'][string],
    },
  };
}

function fakeStatusRunner(
  dialect: 'postgres' | 'snowflake' | 'bigquery',
  catalogName: string,
) {
  return {
    dialect,
    catalogName,
    async run() {
      return { warnings: [], info: [] };
    },
    formatSuccessDetail(result: unknown) {
      const typed = result as { warnings: string[]; info?: string[]; pgServerVersion?: string };
      const info = typed.info && typed.info.length > 0 ? `; ${typed.info.join('; ')}` : '';
      const base =
        dialect === 'postgres'
          ? `pg_stat_statements ready (${typed.pgServerVersion ?? 'PostgreSQL 16.4'})`
          : `${catalogName} ready`;
      return { detail: `${base}${info}`, warnings: typed.warnings };
    },
    fixAdvice(error: unknown) {
      return {
        failHeadline: error instanceof Error ? error.message : String(error),
        remediation: 'Fix query-history grants.',
      };
    },
  };
}

describe('buildProjectStatus query history dispatch', () => {
  it('runs the shared probe for snowflake connections', async () => {
    let probeCalls = 0;
    const runner = fakeStatusRunner(
      'snowflake',
      'SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY',
    );
    const project = projectWithConfig(withSnowflakeQueryHistory(baseProjectConfig()));

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
      queryHistoryReadinessProbe: async (input) => {
        probeCalls += 1;
        expect(input.connectionId).toBe('warehouse');
        return {
          ok: true,
          dialect: 'snowflake',
          runner,
          result: { warnings: [], info: [] },
        };
      },
    });

    expect(probeCalls).toBe(1);
    expect(status.queryHistory).toHaveLength(1);
    expect(status.queryHistory[0]).toMatchObject({
      connection: 'warehouse',
      driver: 'snowflake',
      dialect: 'snowflake',
      status: 'ok',
    });
    expect(status.queryHistory[0].detail).toMatch(/SNOWFLAKE\.ACCOUNT_USAGE\.QUERY_HISTORY/);
    expect(status.queryHistory[0].fix).toBeUndefined();
    expect(status.verdict).not.toBe('blocked');
  });

  it('reports snowflake probe failures with the reader-provided remediation', async () => {
    const project = projectWithConfig(withSnowflakeQueryHistory(baseProjectConfig()));

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
      queryHistoryReadinessProbe: async () => ({
        ok: false,
        dialect: 'snowflake',
        runner: {
          ...fakeStatusRunner('snowflake', 'SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY'),
          fixAdvice: () => ({
            failHeadline: 'Snowflake role cannot read SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY',
            remediation: 'GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE ktx;',
          }),
        },
        error: new Error('role cannot read SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY'),
      }),
    });

    expect(status.queryHistory[0]).toMatchObject({
      connection: 'warehouse',
      driver: 'snowflake',
      dialect: 'snowflake',
      status: 'fail',
      fix: 'GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE ktx;',
    });
    expect(status.queryHistory[0].detail).not.toMatch(/Set connections.*driver to postgres/);
  });

  it('runs the bigquery probe for bigquery connections', async () => {
    let probeCalls = 0;
    const runner = fakeStatusRunner('bigquery', 'INFORMATION_SCHEMA.JOBS_BY_PROJECT');
    const project = projectWithConfig(withBigQueryQueryHistory(baseProjectConfig()));

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
      queryHistoryReadinessProbe: async (input) => {
        probeCalls += 1;
        expect(input.connectionId).toBe('bq');
        return {
          ok: true,
          dialect: 'bigquery',
          runner,
          result: { warnings: [], info: [] },
        };
      },
    });

    expect(probeCalls).toBe(1);
    expect(status.queryHistory[0]).toMatchObject({
      connection: 'bq',
      driver: 'bigquery',
      dialect: 'bigquery',
      status: 'ok',
    });
    expect(status.queryHistory[0].detail).toMatch(/INFORMATION_SCHEMA\.JOBS_BY_PROJECT/);
  });

  it('fails with an accurate message for drivers without a query history reader', async () => {
    const project = projectWithConfig(withMysqlQueryHistory(baseProjectConfig()));

    const status = await buildProjectStatus(project, {
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
      queryHistoryReadinessProbe: async () => {
        throw new Error('postgres probe must not run for mysql');
      },
    });

    expect(status.queryHistory).toHaveLength(1);
    expect(status.queryHistory[0]).toMatchObject({
      connection: 'legacy',
      driver: 'mysql',
      dialect: 'mysql',
      status: 'fail',
      detail: 'query history is not supported for driver "mysql"',
    });
    expect(status.queryHistory[0].fix).toMatch(
      /Disable connections\.legacy\.context\.queryHistory/,
    );
    expect(status.queryHistory[0].fix).not.toMatch(/driver to postgres/);
  });
});

describe('buildProjectStatus --fast', () => {
  it('skips claude-code probe and Postgres query-history probe', async () => {
    let claudeProbeCalls = 0;
    let queryHistoryProbeCalls = 0;
    const project = projectWithConfig(withPostgresQueryHistory(baseProjectConfig()));

    const status = await buildProjectStatus(project, {
      env: { ANALYTICS_DATABASE_URL: 'postgres://example' },
      fast: true,
      claudeCodeAuthProbe: async () => {
        claudeProbeCalls += 1;
        return { ok: true };
      },
      queryHistoryReadinessProbe: async () => {
        queryHistoryProbeCalls += 1;
        throw new Error('should not be called');
      },
    });

    expect(claudeProbeCalls).toBe(0);
    expect(queryHistoryProbeCalls).toBe(0);
    expect(status.llm.status).toBe('skipped');
    expect(status.llm.detail).toMatch(/--fast/);
    expect(status.queryHistory).toHaveLength(1);
    expect(status.queryHistory[0]).toMatchObject({
      connection: 'analytics',
      status: 'skipped',
    });
    expect(status.verdict).not.toBe('blocked');
  });

  it('does not call probes lazily when fast and reports skipped in render', async () => {
    const project = projectWithConfig(withPostgresQueryHistory(baseProjectConfig()));
    const status = await buildProjectStatus(project, {
      env: { ANALYTICS_DATABASE_URL: 'postgres://example' },
      fast: true,
      claudeCodeAuthProbe: stubClaudeCodeAuthProbe,
      queryHistoryReadinessProbe: async () => {
        throw new Error('should not be called');
      },
    });
    const rendered = renderProjectStatus(status, { verbose: false, useColor: false });
    expect(rendered).toContain('auth probe skipped (--fast)');
    expect(rendered).toContain('pg_stat_statements probe skipped (--fast)');
  });
});

describe('buildLocalStatsStatus', () => {
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await mkdtemp(join(tmpdir(), 'ktx-status-stats-'));
  });

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

  function projectIn(dir: string): KtxLocalProject {
    return {
      projectDir: dir,
      configPath: join(dir, 'ktx.yaml'),
      config: baseProjectConfig(),
      coreConfig: {} as KtxLocalProject['coreConfig'],
      git: {} as KtxLocalProject['git'],
      fileStore: {} as KtxLocalProject['fileStore'],
    };
  }

  it('returns unavailable when .ktx/db.sqlite is missing', async () => {
    const stats = await buildLocalStatsStatus(projectIn(tempDir));
    expect(stats.unavailable).toMatch(/no \.ktx\/db\.sqlite/);
    expect(stats.ingest.totalCompletedRuns).toBe(0);
    expect(stats.projectDir.dbSqliteBytes).toBeNull();
  });

  it('reads counts from a seeded SQLite DB and walks projectDir folders', async () => {
    await mkdir(join(tempDir, '.ktx'), { recursive: true });
    await mkdir(join(tempDir, '.ktx', 'cache'), { recursive: true });
    await writeFile(join(tempDir, '.ktx', 'cache', 'a.bin'), Buffer.alloc(2048));
    await mkdir(join(tempDir, 'raw-sources', 'analytics'), { recursive: true });
    await writeFile(join(tempDir, 'raw-sources', 'analytics', 'snap.json'), 'x'.repeat(100));
    await writeFile(join(tempDir, 'raw-sources', 'analytics', 'snap.bin'), Buffer.alloc(512));
    await mkdir(join(tempDir, 'wiki', 'global', 'sub'), { recursive: true });
    await writeFile(join(tempDir, 'wiki', 'global', 'one.md'), '# one');
    await writeFile(join(tempDir, 'wiki', 'global', 'sub', 'two.md'), '# two');
    await mkdir(join(tempDir, 'semantic-layer'), { recursive: true });
    await writeFile(join(tempDir, 'semantic-layer', 'orders.yaml'), 'name: orders');
    await writeFile(join(tempDir, 'semantic-layer', 'users.yml'), 'name: users');

    const dbPath = join(tempDir, '.ktx', 'db.sqlite');
    const db = new Database(dbPath);
    db.exec(`
      CREATE TABLE local_ingest_reports (
        run_id TEXT PRIMARY KEY,
        adapter TEXT NOT NULL,
        connection_id TEXT NOT NULL,
        status TEXT NOT NULL,
        completed_at TEXT NOT NULL,
        raw_content_hashes_json TEXT NOT NULL,
        body_json TEXT NOT NULL
      );
      INSERT INTO local_ingest_reports VALUES
        ('r1', 'live-database', 'analytics', 'done', '2026-04-01T10:00:00Z', '{}', '{}'),
        ('r2', 'live-database', 'analytics', 'done', '2026-05-10T10:00:00Z', '{}', '{}'),
        ('r3', 'notion', 'docs', 'done', '2026-05-01T10:00:00Z', '{}', '{}'),
        ('r4', 'notion', 'docs', 'error', '2026-05-02T10:00:00Z', '{}', '{}');

      CREATE TABLE knowledge_pages (
        path TEXT PRIMARY KEY,
        key TEXT NOT NULL,
        scope TEXT NOT NULL,
        scope_id TEXT,
        summary TEXT NOT NULL,
        content TEXT NOT NULL,
        tags TEXT NOT NULL,
        search_text TEXT NOT NULL DEFAULT '',
        embedding_json TEXT
      );
      INSERT INTO knowledge_pages VALUES
        ('a.md', 'a', 'GLOBAL', NULL, '', '', '[]', '', '[0.1,0.2]'),
        ('b.md', 'b', 'GLOBAL', NULL, '', '', '[]', '', NULL),
        ('c.md', 'c', 'PROJECT', NULL, '', '', '[]', '', '[0.3,0.4]');

      CREATE TABLE local_sl_sources (
        connection_id TEXT NOT NULL,
        source_name TEXT NOT NULL,
        search_text TEXT NOT NULL,
        embedding_json TEXT,
        content_hash TEXT,
        updated_at TEXT NOT NULL,
        PRIMARY KEY (connection_id, source_name)
      );
      INSERT INTO local_sl_sources VALUES
        ('analytics', 'orders', '', '[0.1,0.2]', NULL, '2026-05-10T10:00:00Z'),
        ('analytics', 'users', '', NULL, NULL, '2026-05-10T10:00:00Z');

      CREATE TABLE local_sl_dictionary_values (
        connection_id TEXT NOT NULL,
        source_name TEXT NOT NULL,
        column_name TEXT NOT NULL,
        value TEXT NOT NULL,
        value_lower TEXT NOT NULL,
        cardinality INTEGER,
        updated_at TEXT NOT NULL,
        PRIMARY KEY (connection_id, source_name, column_name, value)
      );
      INSERT INTO local_sl_dictionary_values VALUES
        ('analytics', 'orders', 'status', 'open', 'open', 1, '2026-05-10T10:00:00Z'),
        ('analytics', 'orders', 'status', 'closed', 'closed', 1, '2026-05-10T10:00:00Z');
    `);
    db.close();

    const stats = await buildLocalStatsStatus(projectIn(tempDir));
    expect(stats.unavailable).toBeUndefined();
    expect(stats.ingest.totalCompletedRuns).toBe(3);
    expect(stats.ingest.perConnection).toEqual([
      { connectionId: 'analytics', adapter: 'live-database', lastCompletedAt: '2026-05-10T10:00:00Z' },
      { connectionId: 'docs', adapter: 'notion', lastCompletedAt: '2026-05-01T10:00:00Z' },
    ]);
    expect(stats.wikiPages).toEqual([
      { scope: 'GLOBAL', count: 2, embeddedCount: 1 },
      { scope: 'PROJECT', count: 1, embeddedCount: 1 },
    ]);
    expect(stats.semanticLayer).toEqual([
      {
        connectionId: 'analytics',
        sourceCount: 2,
        embeddedSourceCount: 1,
        dictionaryValueCount: 2,
      },
    ]);
    expect(stats.projectDir.dbSqliteBytes).toBeGreaterThan(0);
    expect(stats.projectDir.ktxCacheBytes).toBe(2048);
    expect(stats.projectDir.rawSources).toEqual({ fileCount: 2, bytes: 612 });
  });

  it('tolerates a SQLite DB missing some tables', async () => {
    await mkdir(join(tempDir, '.ktx'), { recursive: true });
    const dbPath = join(tempDir, '.ktx', 'db.sqlite');
    const db = new Database(dbPath);
    db.exec(`
      CREATE TABLE local_ingest_reports (
        run_id TEXT PRIMARY KEY,
        adapter TEXT NOT NULL,
        connection_id TEXT NOT NULL,
        status TEXT NOT NULL,
        completed_at TEXT NOT NULL,
        raw_content_hashes_json TEXT NOT NULL,
        body_json TEXT NOT NULL
      );
      INSERT INTO local_ingest_reports VALUES
        ('r1', 'live-database', 'analytics', 'done', '2026-05-10T10:00:00Z', '{}', '{}');
    `);
    db.close();

    const stats = await buildLocalStatsStatus(projectIn(tempDir));
    expect(stats.unavailable).toBeUndefined();
    expect(stats.ingest.totalCompletedRuns).toBe(1);
    expect(stats.wikiPages).toEqual([]);
    expect(stats.semanticLayer).toEqual([]);
  });
});

describe('renderProjectStatus Local data', () => {
  it('renders the Local data section with seeded stats', async () => {
    const project = projectWithConfig(baseProjectConfig());
    const status = await buildProjectStatus(project, { claudeCodeAuthProbe: stubClaudeCodeAuthProbe });
    status.localStats = {
      ingest: {
        totalCompletedRuns: 3,
        perConnection: [
          { connectionId: 'analytics', adapter: 'live-database', lastCompletedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString() },
        ],
      },
      wikiPages: [
        { scope: 'GLOBAL', count: 2, embeddedCount: 2 },
        { scope: 'PROJECT', count: 1, embeddedCount: 0 },
      ],
      semanticLayer: [
        {
          connectionId: 'analytics',
          sourceCount: 12,
          embeddedSourceCount: 10,
          dictionaryValueCount: 200,
        },
      ],
      projectDir: {
        dbSqliteBytes: 4096,
        ktxCacheBytes: 1_048_576,
        rawSources: { fileCount: 5, bytes: 200 },
      },
    };
    const rendered = renderProjectStatus(status, { useColor: false });
    expect(rendered).toContain('Local data');
    expect(rendered).toContain('Wiki');
    expect(rendered).not.toContain('Knowledge');
    expect(rendered).toContain('3 completed runs');
    expect(rendered).toContain('GLOBAL=2 (2 embedded)');
    expect(rendered).toContain('PROJECT=1 (0 embedded)');
    expect(rendered).toContain('12 sources (10 embedded) · 200 dictionary values');
    expect(rendered).toContain('db=4.00 KiB');
    expect(rendered).toContain('cache=1.00 MiB');
    expect(rendered).not.toMatch(/wiki=\d+ md/);
    expect(rendered).not.toMatch(/semantic-layer=\d+ yaml/);
  });

  it('renders unavailable note when DB is missing', async () => {
    const project = projectWithConfig(baseProjectConfig());
    const status = await buildProjectStatus(project, { claudeCodeAuthProbe: stubClaudeCodeAuthProbe });
    status.localStats = {
      ingest: { totalCompletedRuns: 0, perConnection: [] },
      wikiPages: [],
      semanticLayer: [],
      projectDir: {
        dbSqliteBytes: null,
        ktxCacheBytes: 0,
        rawSources: { fileCount: 0, bytes: 0 },
      },
      unavailable: 'no .ktx/db.sqlite yet',
    };
    const rendered = renderProjectStatus(status, { useColor: false });
    expect(rendered).toContain('Local data');
    expect(rendered).toContain('no .ktx/db.sqlite yet');
  });
});
