import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import {
  isSupabasePoolerUrl,
  deriveDirectUrl,
  normalizeDirectUrl,
  readKillSwitchEnv,
  isNetworkUnreachableError,
  resolveDirectPoolSize,
  ConnectionManager,
  DEFAULT_DIRECT_POOL_SIZE,
} from '../src/core/connection-manager.ts';

describe('isSupabasePoolerUrl', () => {
  test('detects port 6543', () => {
    expect(isSupabasePoolerUrl('postgresql://u:p@host:6543/db')).toBe(true);
  });

  test('detects pooler.supabase.com hostname', () => {
    expect(
      isSupabasePoolerUrl('postgresql://u:p@aws-0-us-east-1.pooler.supabase.com:5432/db')
    ).toBe(true);
  });

  test('rejects direct supabase host', () => {
    expect(
      isSupabasePoolerUrl('postgresql://u:p@db.abc.supabase.co:5432/postgres')
    ).toBe(false);
  });

  test('rejects self-hosted on standard port', () => {
    expect(isSupabasePoolerUrl('postgresql://u:p@localhost:5432/gbrain_test')).toBe(false);
  });

  test('handles malformed URL gracefully', () => {
    expect(isSupabasePoolerUrl('not a url')).toBe(false);
  });
});

describe('deriveDirectUrl', () => {
  test('swaps pooler hostname + port for known shape', () => {
    const direct = deriveDirectUrl(
      'postgresql://postgres.abcxyz:secret@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
    );
    expect(direct).toBeTruthy();
    expect(direct).toContain('db.abcxyz.supabase.co:5432');
    expect(direct).toContain(':secret@'); // creds preserved
  });

  test('strips .<project-ref> suffix from username when going pooler→direct', () => {
    // Supabase direct connections require bare `postgres`; the `postgres.<ref>`
    // form is pooler-only (Supavisor uses the suffix for tenant routing).
    // Without the strip, direct auth fails with "password authentication
    // failed for user postgres.<ref>" even with the correct password.
    const direct = deriveDirectUrl(
      'postgresql://postgres.abcxyz:secret@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
    );
    expect(direct).toContain('postgres:secret@'); // bare username
    expect(direct).not.toContain('postgres.abcxyz:secret@'); // no pooler suffix
  });

  test('falls back to port-only swap when project-ref unparseable', () => {
    const direct = deriveDirectUrl(
      'postgresql://customuser:secret@some.pooler.supabase.com:6543/db'
    );
    expect(direct).toBeTruthy();
    expect(direct).toContain(':5432');
    expect(direct).toContain('some.pooler.supabase.com'); // host preserved
    expect(direct).toContain('customuser:secret@'); // non-pooler username preserved
  });

  test('returns null for non-pooler URL', () => {
    expect(deriveDirectUrl('postgresql://u:p@localhost:5432/db')).toBeNull();
  });

  test('preserves query string', () => {
    const direct = deriveDirectUrl(
      'postgresql://postgres.ref:p@aws.pooler.supabase.com:6543/db?prepare=false'
    );
    expect(direct).toContain('?prepare=false');
  });
});

describe('normalizeDirectUrl', () => {
  test('normalizes a transaction-pooler (6543) override to the real direct host', () => {
    const direct = normalizeDirectUrl(
      'postgresql://postgres.abcxyz:p@aws-0-us-west-2.pooler.supabase.com:6543/postgres',
      'postgresql://postgres.abcxyz:p@aws-0-us-west-2.pooler.supabase.com:6543/postgres',
    );
    expect(direct).toBe('postgresql://postgres:p@db.abcxyz.supabase.co:5432/postgres');
  });

  test('keeps a non-pooler direct override', () => {
    const direct = normalizeDirectUrl(
      'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      'postgresql://u:p@custom-direct.example.com:5432/db',
    );
    expect(direct).toBe('postgresql://u:p@custom-direct.example.com:5432/db');
  });

  test('keeps a session-mode pooler override (pooler host, port 5432)', () => {
    const sessionUrl = 'postgresql://postgres.abc:p@aws.pooler.supabase.com:5432/db';
    const direct = normalizeDirectUrl(
      'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      sessionUrl,
    );
    expect(direct).toBe(sessionUrl);
  });

  test('no override: derives from the primary as before', () => {
    const direct = normalizeDirectUrl(
      'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
    );
    expect(direct).toContain('db.abc.supabase.co:5432');
  });

  test('no override, non-Supabase primary: null', () => {
    expect(normalizeDirectUrl('postgresql://u:p@localhost:5432/db')).toBeNull();
  });
});

describe('readKillSwitchEnv', () => {
  let original: string | undefined;
  beforeEach(() => { original = process.env.GBRAIN_DISABLE_DIRECT_POOL; });
  afterEach(() => {
    if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
  });

  test('false when unset', () => {
    delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    expect(readKillSwitchEnv()).toBe(false);
  });

  test('true when "1"', () => {
    process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
    expect(readKillSwitchEnv()).toBe(true);
  });

  test('true when "true"', () => {
    process.env.GBRAIN_DISABLE_DIRECT_POOL = 'true';
    expect(readKillSwitchEnv()).toBe(true);
  });

  test('false for any other value', () => {
    process.env.GBRAIN_DISABLE_DIRECT_POOL = '0';
    expect(readKillSwitchEnv()).toBe(false);
    process.env.GBRAIN_DISABLE_DIRECT_POOL = 'false';
    expect(readKillSwitchEnv()).toBe(false);
  });
});

describe('resolveDirectPoolSize', () => {
  let original: string | undefined;
  beforeEach(() => { original = process.env.GBRAIN_DIRECT_POOL_SIZE; });
  afterEach(() => {
    if (original === undefined) delete process.env.GBRAIN_DIRECT_POOL_SIZE;
    else process.env.GBRAIN_DIRECT_POOL_SIZE = original;
  });

  test('default to 3', () => {
    delete process.env.GBRAIN_DIRECT_POOL_SIZE;
    expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
    expect(DEFAULT_DIRECT_POOL_SIZE).toBe(3);
  });

  test('explicit overrides env', () => {
    process.env.GBRAIN_DIRECT_POOL_SIZE = '5';
    expect(resolveDirectPoolSize(7)).toBe(7);
  });

  test('env overrides default', () => {
    process.env.GBRAIN_DIRECT_POOL_SIZE = '5';
    expect(resolveDirectPoolSize()).toBe(5);
  });

  test('rejects invalid env values', () => {
    process.env.GBRAIN_DIRECT_POOL_SIZE = 'abc';
    expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
    process.env.GBRAIN_DIRECT_POOL_SIZE = '0';
    expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
    process.env.GBRAIN_DIRECT_POOL_SIZE = '999';
    expect(resolveDirectPoolSize()).toBe(DEFAULT_DIRECT_POOL_SIZE);
  });
});

describe('ConnectionManager — describeMode + dual-pool routing', () => {
  let originalKillSwitch: string | undefined;
  let originalDirectUrl: string | undefined;
  beforeEach(() => {
    originalKillSwitch = process.env.GBRAIN_DISABLE_DIRECT_POOL;
    originalDirectUrl = process.env.GBRAIN_DIRECT_DATABASE_URL;
    delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    delete process.env.GBRAIN_DIRECT_DATABASE_URL;
  });
  afterEach(() => {
    if (originalKillSwitch === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    else process.env.GBRAIN_DISABLE_DIRECT_POOL = originalKillSwitch;
    if (originalDirectUrl === undefined) delete process.env.GBRAIN_DIRECT_DATABASE_URL;
    else process.env.GBRAIN_DIRECT_DATABASE_URL = originalDirectUrl;
  });

  test('non-Supabase URL → single mode', () => {
    const cm = new ConnectionManager({ url: 'postgresql://u:p@localhost:5432/db' });
    expect(cm.isSupabase()).toBe(false);
    expect(cm.isDualPoolActive()).toBe(false);
    expect(cm.describeMode().mode).toBe('single (non-supabase)');
  });

  test('Supabase pooler URL → dual mode (without kill-switch)', () => {
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
    });
    expect(cm.isSupabase()).toBe(true);
    expect(cm.isDualPoolActive()).toBe(true);
    expect(cm.describeMode().mode).toBe('split');
    expect(cm.describeMode().direct_host).toContain('db.abc.supabase.co:5432');
  });

  test('kill-switch active → single mode (kill-switch)', () => {
    process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
    });
    expect(cm.isSupabase()).toBe(true);
    expect(cm.isKillSwitchActive()).toBe(true);
    expect(cm.isDualPoolActive()).toBe(false);
    expect(cm.describeMode().mode).toBe('single (kill-switch)');
  });

  test('explicit directUrl override wins', () => {
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      directUrl: 'postgresql://u:p@custom-direct.example.com:5432/db',
    });
    expect(cm.resolveDirectUrl()).toContain('custom-direct.example.com');
  });

  test('explicit transaction-pooler directUrl override is normalized to direct host', () => {
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      directUrl: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
    });
    expect(cm.resolveDirectUrl()).toContain('db.abc.supabase.co:5432');
    expect(cm.resolveDirectUrl()).not.toContain(':6543');
  });

  test('env transaction-pooler directUrl override is normalized to direct host', () => {
    process.env.GBRAIN_DIRECT_DATABASE_URL =
      'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db';
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
    });
    expect(cm.resolveDirectUrl()).toContain('db.abc.supabase.co:5432');
    expect(cm.resolveDirectUrl()).not.toContain(':6543');
  });

  test('host string contains creds neither in describeMode nor resolveDirectUrl logging', () => {
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:secret@aws.pooler.supabase.com:6543/db',
    });
    const desc = cm.describeMode();
    expect(desc.direct_host ?? '').not.toContain('secret');
  });
});

describe('ConnectionManager — parent inheritance (A2)', () => {
  test('child inherits kill-switch from parent', () => {
    const original = process.env.GBRAIN_DISABLE_DIRECT_POOL;
    try {
      process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
      const parent = new ConnectionManager({
        url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      });
      // Child constructed AFTER env reset — parent's snapshot is what matters.
      delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
      const child = new ConnectionManager({
        url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
        parent,
      });
      expect(child.isKillSwitchActive()).toBe(true);
      expect(child.isDualPoolActive()).toBe(false);
    } finally {
      if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
      else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
    }
  });

  test('child without parent reads env at construction', () => {
    const original = process.env.GBRAIN_DISABLE_DIRECT_POOL;
    try {
      delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
      const cm = new ConnectionManager({
        url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      });
      expect(cm.isKillSwitchActive()).toBe(false);
      // Mutating env after construction does NOT change the manager's state.
      process.env.GBRAIN_DISABLE_DIRECT_POOL = '1';
      expect(cm.isKillSwitchActive()).toBe(false); // snapshot semantics
    } finally {
      if (original === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
      else process.env.GBRAIN_DISABLE_DIRECT_POOL = original;
    }
  });
});

describe('isNetworkUnreachableError (#1641)', () => {
  test('classifies network codes as unreachable', () => {
    for (const code of ['ENOTFOUND', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'ETIMEDOUT', 'CONNECT_TIMEOUT']) {
      const err = Object.assign(new Error('connect failed'), { code });
      expect(isNetworkUnreachableError(err)).toBe(true);
    }
  });

  test('classifies by message when code absent', () => {
    expect(isNetworkUnreachableError(new Error('getaddrinfo ENOTFOUND db.abc.supabase.co'))).toBe(true);
  });

  test('auth/SQL errors are NOT unreachable', () => {
    expect(isNetworkUnreachableError(new Error('password authentication failed for user "postgres"'))).toBe(false);
    expect(isNetworkUnreachableError(new Error('syntax error at or near "SELEC"'))).toBe(false);
    expect(isNetworkUnreachableError(null)).toBe(false);
  });
});

describe('ConnectionManager — direct-pool fallback on unreachable host (#1641)', () => {
  let originalKillSwitch: string | undefined;
  let originalError: typeof console.error;
  let errLines: string[];
  beforeEach(() => {
    originalKillSwitch = process.env.GBRAIN_DISABLE_DIRECT_POOL;
    delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    originalError = console.error;
    errLines = [];
    console.error = (...args: unknown[]) => { errLines.push(args.join(' ')); };
  });
  afterEach(() => {
    console.error = originalError;
    if (originalKillSwitch === undefined) delete process.env.GBRAIN_DISABLE_DIRECT_POOL;
    else process.env.GBRAIN_DISABLE_DIRECT_POOL = originalKillSwitch;
  });

  test('ddl() falls back to the read pool when the direct host is unreachable', async () => {
    const cm = new ConnectionManager({
      url: 'postgresql://postgres.abc:p@aws.pooler.supabase.com:6543/db',
      // 127.0.0.1:9 (discard) → instant ECONNREFUSED, the IPv4-only-network shape.
      directUrl: 'postgresql://postgres:p@127.0.0.1:9/db',
    });
    const fakeReadPool = {} as ReturnType<typeof ConnectionManager.prototype.read>;
    cm.setReadPool(fakeReadPool);
    expect(cm.isDualPoolActive()).toBe(true);

    const pool = await cm.ddl(); // without the fix this throws ECONNREFUSED
    expect(pool).toBe(fakeReadPool);
    // Self-activating kill-switch: subsequent calls skip the direct pool.
    expect(cm.isKillSwitchActive()).toBe(true);
    expect(cm.isDualPoolActive()).toBe(false);
    expect(cm.describeMode().mode).toBe('single (kill-switch)');
    // One stderr line mentioning the power-user override.
    const warning = errLines.filter(l => l.includes('GBRAIN_DIRECT_DATABASE_URL'));
    expect(warning.length).toBe(1);

    const again = await cm.ddl();
    expect(again).toBe(fakeReadPool);
    expect(errLines.filter(l => l.includes('GBRAIN_DIRECT_DATABASE_URL')).length).toBe(1);
  }, 20000);
});
