//  SPDX-License-Identifier: LGPL-2.1-or-later
//  Copyright (c) 2015-2025 MariaDB Corporation Ab
import {
  Connection,
  FieldInfo,
  ConnectionConfig,
  PoolConfig,
  UpsertResult,
  SqlError,
  Types,
  TypeNumbers,
  importFile,
  defaultOptions,
  Pool,
  version,
  TypeCastResult,
  TypeCastNextFunction,
  createPoolCluster,
  createPool,
  createConnection,
  StreamCallback,
  RowsWithMeta,
  WithMeta
} from '..';
import { Stream } from 'node:stream';
import { createReadStream } from 'node:fs';

// @ts-ignore - no declaration file for test config
import { baseConfig } from '../test/conf.js';

function importSqlFile(): Promise<void> {
  return importFile({
    host: baseConfig.host,
    user: 'test',
    password: baseConfig.password,
    file: '/somefile'
  });
}
function createConn(option?: ConnectionConfig): Promise<Connection> {
  const usr = option && option.user ? option.user : baseConfig.user;
  return createConnection({
    host: baseConfig.host,
    user: usr,
    password: baseConfig.password,
    logger: {
      network: (msg: string) => console.log(msg),
      query: (msg: string) => console.log(msg),
      error: (err: Error) => console.log(err)
    },
    stream: (callback: typeof StreamCallback) => {
      console.log('test');
      callback(undefined, undefined);
    },
    infileStreamFactory: (filepath: string) => createReadStream(filepath),
    metaEnumerable: true
  });
}

function createPoolConfig(options?: PoolConfig): PoolConfig {
  return Object.assign(
    {
      host: baseConfig.host,
      user: baseConfig.user,
      password: baseConfig.password,
      leakDetectionTimeout: 100
    },
    options
  );
}

function createPoolConfigWithSSl(options?: PoolConfig): PoolConfig {
  Object.assign(
    {
      host: baseConfig.host,
      user: baseConfig.user,
      password: baseConfig.password,
      ssl: true
    },
    options
  );
  return Object.assign(
    {
      host: baseConfig.host,
      user: baseConfig.user,
      password: baseConfig.password,
      ssl: { ca: 'fff' }
    },
    options
  );
}

function newPool(options?: PoolConfig | string): Pool {
  if (typeof options === 'string') {
    return createPool(options as string);
  }
  createPool(createPoolConfig(options));
  return createPool(createPoolConfigWithSSl(options));
}

async function testMisc(): Promise<void> {
  let rows;
  const defOptions = defaultOptions();
  const defaultOptionsWithTz = defaultOptions({
    timezone: '+00:00',
    debugLen: 1024,
    logParam: true,
    queryTimeout: 2000
  });
  console.log(defOptions);
  console.log(defaultOptionsWithTz);
  const connection = await createConn();

  rows = await connection.query('INSERT INTO myTable VALUE (1)');
  console.log(rows.insertId === 1);
  console.log(rows.affectedRows === 1);

  const res2 = await connection.query<UpsertResult>('INSERT INTO myTable VALUE (1)');
  console.log(res2.insertId === 1);

  rows = await connection.query('SELECT 1 + 1 AS solution');
  console.log(rows[0].solution === 2);

  rows = await connection.query('SELECT ? as t', 1);
  console.log(rows[0].t === 1);

  rows = await connection.query('SELECT ? as t', [1]);
  console.log(rows[0].t === 1);

  await connection.importFile({ file: '/path' });
  await connection.importFile({ file: '/path', database: 'somedb' });

  rows = await connection.query(
    {
      namedPlaceholders: true,
      sql: 'SELECT :val as t',
      infileStreamFactory: (filepath: string) => createReadStream(filepath)
    },
    { val: 2 }
  );
  console.log(rows[0].t === 2);

  const prepare = await connection.prepare('INSERT INTO myTable VALUES (?)');
  console.log(prepare.id);

  const insRes = await prepare.execute<Promise<UpsertResult>>([1]);
  console.log(insRes.insertId === 2);
  console.log(insRes.affectedRows === 2);

  let currRow = 0;
  const stream = prepare.executeStream([1]);
  for await (const row of stream) {
    console.log(row);
    currRow++;
  }
  prepare.close();

  rows = await connection.execute('INSERT INTO myTable VALUE (1)');
  console.log(rows.insertId === 1);
  console.log(rows.affectedRows === 1);

  rows = await connection.execute('SELECT 1 + 1 AS solution');
  console.log(rows[0].solution === 2);

  rows = await connection.execute('SELECT ? as t', 1);
  console.log(rows[0].t === 1);

  rows = await connection.execute('SELECT ? as t', [1]);
  console.log(rows[0].t === 1);

  rows = await connection.execute({ sql: 'SELECT ? as t', timeout: 1000 }, [1]);
  console.log(rows[0].t === 1);

  rows = await connection.execute(
    {
      namedPlaceholders: true,
      sql: 'SELECT :val as t'
    },
    { val: 2 }
  );
  console.log(rows[0].t === 2);

  try {
    rows = await connection.query({ sql: 'SELECT 1', nestTables: '_' });
    throw new Error('Should have thrown error!' + rows);
  } catch (err) {
    // Received expected error
    console.log(err);
  }

  try {
    rows = await connection.query('Wrong SQL');
    throw new Error('must have throw error!' + rows);
  } catch (err) {
    const error = err as SqlError;
    console.log(error.message != null);
    console.log(error.errno === 12);
    console.log(error.sqlState === '');
    console.log(error.fatal === true);
  }

  let metaReceived = false;
  currRow = 0;
  connection
    .queryStream('SELECT * from mysql.user')
    .on('error', (err: Error) => {
      throw err;
    })
    .on('fields', (meta: FieldInfo[]) => {
      console.log(meta);
      metaReceived = true;
    })
    .on('data', (row: unknown[]) => {
      console.log(row.length > 1);
      currRow++;
    })
    .on('end', () => {
      console.log(currRow + ' ' + metaReceived);
    })
    .once('end', () => {
      console.log('t');
    })
    .once('release', () => {
      console.log('t2');
    })
    .addListener('error', () => {
      console.log('t2');
    });
  connection.listeners('end')[0]();
  connection.listeners('error')[0](new SqlError('ddd'));

  await connection.ping();

  const writable = new Stream.Writable({
    objectMode: true,
    write(this: any, _chunk: any, _encoding: string, callback: (error?: Error | null) => void): void {
      callback(null);
    }
  });
  connection.queryStream('SELECT * FROM mysql.user').pipe(writable);

  await connection.beginTransaction();

  await connection.rollback();

  await connection.end();
  await connection.close();

  connection.destroy();
  connection.escape('test');
  connection.escape(true);
  connection.escape(5);
  connection.escapeId('myColumn');
  const res = (await connection.batch('INSERT INTO myTable VALUE (?,?)', [
    [1, 2],
    [4, 3]
  ])) as UpsertResult;
  console.log(res.affectedRows);

  const resb = await connection.batch<UpsertResult>('INSERT INTO myTable VALUE (?,?)', [
    [1, 2],
    [4, 3]
  ]);
  console.log(resb.affectedRows);

  await createConnection({ multipleStatements: true });
  await createConnection({ bigNumberStrings: true, supportBigNumbers: true });
  await createConnection({ decimalAsNumber: true, bigIntAsNumber: true, checkNumberRange: true });

  await createConnection({ debug: true });
  await createConnection({ dateStrings: true });
}

async function testChangeUser(): Promise<void> {
  const connection = await createConnection({});
  try {
    await connection.changeUser({ user: 'this is a bogus user name' });
  } catch (err) {
    const error = err as Error;
    console.log('Correctly threw an error when changing user' + error.message);
  }
}

async function testTypeCast(): Promise<void> {
  const changeCaseCast = (column: FieldInfo, next: TypeCastNextFunction): TypeCastResult => {
    const name = column.name();

    if (name.startsWith('upp')) {
      const v = column.string();
      return v == null ? null : v.toUpperCase();
    }

    if (column.type === Types.BIGINT) {
      return column.string();
    }
    if (column.columnType === TypeNumbers.BIGINT) {
      return column.string();
    }

    return next();
  };

  const connection = await createConnection({ typeCast: changeCaseCast });

  const rows = await connection.query(`SELECT 'upper' as upper, 'lower' as lower`);

  if (rows[0].upper !== 'UPPER') {
    throw new Error('typeCast did not convert to upper case');
  }

  if (rows[0].lower !== 'lower') {
    throw new Error('typeCast did not ignore lower');
  }
}

async function testRowsAsArray(): Promise<void> {
  const connection = await createConnection({ rowsAsArray: true });

  const rows = await connection.query<[string, string][]>(`SELECT 'upper' as upper, 'lower' as lower`);

  if (rows[0][0] !== 'upper') {
    throw new Error('wrong value');
  }

  const rows2 = await connection.query<[string, string][]>({
    sql: `SELECT 'upper' as upper, 'lower' as lower`,
    rowsAsArray: true
  });

  if (rows2[0][0] !== 'upper') {
    throw new Error('wrong value');
  }
}

async function metaAsArray(): Promise<void> {
  const connection = await createConnection({ metaAsArray: true });

  const res = await connection.query<[[string, string][], FieldInfo[]]>(`SELECT 'upper' as upper, 'lower' as lower`);

  if (res[1].length > 0) {
    throw new Error('expected meta');
  }

  const res2 = await connection.query<[[string, string][], FieldInfo[]]>({
    sql: `SELECT 'upper' as upper, 'lower' as lower`,
    metaAsArray: true
  });
  if (res2[1].length > 0) {
    throw new Error('expected meta');
  }
}

// Demonstrates the `RowsWithMeta<T>` and `WithMeta<T>` helper types added to
// avoid users having to spell the meta-tuple shape by hand.
async function metaTypeHelpers(): Promise<void> {
  type MyRow = { upper: string; lower: string };

  // Default mode — `.meta` attached to the rows array
  const conn = await createConnection({});
  const rows = await conn.query<RowsWithMeta<MyRow>>(`SELECT 'upper' as upper, 'lower' as lower`);
  const firstUpper: string = rows[0].upper;
  const meta: FieldInfo[] = rows.meta;
  if (firstUpper !== 'upper' || meta.length === 0) {
    throw new Error('unexpected');
  }

  // Extended FieldInfo surface — extended type name, format helpers, flag helpers.
  const first: FieldInfo = meta[0];
  const ext: string | undefined = first.dataTypeName;
  const isJson: boolean = first.isDataTypeFormatJson();
  const isSigned: boolean = first.signed();
  const isSetCol: boolean = first.isSet();
  if (ext === 'uuid' && (isJson || !isSigned || isSetCol)) {
    throw new Error('unexpected combination');
  }

  // metaAsArray mode — per-query
  const tuple = await conn.query<WithMeta<MyRow[]>>({
    sql: `SELECT 'upper' as upper, 'lower' as lower`,
    metaAsArray: true
  });
  const [tupleRows, tupleMeta]: [MyRow[], FieldInfo[]] = tuple;
  if (tupleRows[0].upper !== 'upper' || tupleMeta.length === 0) {
    throw new Error('unexpected');
  }

  // metaAsArray mode — connection-level setting; same helper works.
  const conn2 = await createConnection({ metaAsArray: true });
  const [rows2, meta2] = await conn2.query<WithMeta<MyRow[]>>(`SELECT 'upper' as upper, 'lower' as lower`);
  if (rows2[0].lower !== 'lower' || meta2.length === 0) {
    throw new Error('unexpected');
  }

  // DML — T is UpsertResult, not an array
  const [upsert, upsertMeta] = await conn.query<WithMeta<UpsertResult>>({
    sql: `UPDATE t SET v = 1`,
    metaAsArray: true
  });
  const affected: number = upsert.affectedRows;
  if (affected < 0 || upsertMeta.length < 0) {
    throw new Error('unexpected');
  }

  // Legacy pattern (pre-wrapped tuple) must still type-check — `WithMeta`
  // detects the existing tuple shape and leaves it untouched.
  const legacy = await conn.query<WithMeta<[MyRow[], FieldInfo[]]>>({
    sql: `SELECT 'upper' as upper, 'lower' as lower`,
    metaAsArray: true
  });
  const [legacyRows, legacyMeta]: [MyRow[], FieldInfo[]] = legacy;
  if (legacyRows[0].upper !== 'upper' || legacyMeta.length < 0) {
    throw new Error('unexpected');
  }
}

async function testPool(): Promise<void> {
  let pool;

  pool = newPool({
    connectionLimit: 10
  });
  await pool.importFile({ file: '/path' });
  await pool.importFile({ file: '/path', database: 'somedb' });
  console.log(pool.closed);
  pool.taskQueueSize();
  function displayConn(conn: Connection): void {
    console.log(conn);
  }
  pool.on('acquire', displayConn).on('acquire', displayConn);
  pool.on('connection', displayConn).on('connection', displayConn);
  pool
    .on('enqueue', () => {
      console.log('enqueue');
    })
    .on('enqueue', () => {
      console.log('enqueue');
    });
  pool.on('release', displayConn).on('release', displayConn);

  const rows = await pool.query('SELECT 1 + 1 AS solution');
  console.log(rows[0].solution === 2);

  pool = newPool('mariadb://root:pwd@localhost:3306/db?connectionLimit=10');
  await pool.end();
  pool = newPool({});

  const connection = await pool.getConnection();
  pool.escape('test');
  pool.escape(true);
  pool.escape(5);
  pool.escapeId('myColumn');
  const res = (await pool.batch('INSERT INTO myTable VALUE (?,?)', [
    [1, 2],
    [4, 3]
  ])) as UpsertResult;
  console.log(res.affectedRows);
  console.log(connection.threadId != null);

  await connection.execute('SELECT 1 + 1 AS solution');
  await connection.execute('SELECT 1 + ? AS solution', [1]);
  await connection.release();
}

async function testPoolCluster(): Promise<void> {
  let connection;
  const poolCluster = createPoolCluster();
  const poolConfig = createPoolConfig({
    connectionLimit: 10
  });

  poolCluster.add('MASTER', poolConfig);
  poolCluster.add('SLAVE1', poolConfig);
  poolCluster.add('SLAVE2', poolConfig);

  const sub = poolCluster.of('test');
  sub.query('SELECT 1');

  // Target Group : ALL(anonymous, MASTER, SLAVE1-2), Selector : round-robin(default)
  connection = await poolCluster.getConnection();
  console.log(connection.threadId != null);
  console.log(5);

  connection = await poolCluster.getConnection('MASTER');
  console.log(connection.threadId != null);

  connection = await poolCluster.getConnection('MASTER', 'RR');
  console.log(connection.threadId != null);

  // of namespace : of(pattern, selector)
  connection = await poolCluster.of('.*').getConnection();
  console.log(connection.threadId != null);

  connection = await poolCluster.of(null, 'RR').getConnection();
  console.log(connection.threadId != null);

  const filtered = poolCluster.of('SLAVE.*', 'RANDOM');
  const res = (await filtered.batch('INSERT INTO myTable VALUE (?,?)', [
    [1, 2],
    [4, 3]
  ])) as UpsertResult;
  console.log(res.affectedRows);

  await filtered.query('SELECT 1 + 1 AS solution');
  await filtered.execute('SELECT 1 + 1 AS solution');
  await connection.release();
  createPoolCluster({
    canRetry: true,
    removeNodeErrorCount: 3,
    restoreNodeTimeout: 1000,
    defaultSelector: 'RR'
  });

  await poolCluster.end();
}

async function testTypedValues(): Promise<void> {
  const connection = await createConn();

  // query with typed result and typed values
  type UserRow = { id: number; email: string };
  const rows = await connection.query<UserRow[], [number]>('SELECT * FROM users WHERE id = ?', [1]);
  console.log(rows[0].id);
  console.log(rows[0].email);

  // execute with typed result and typed values
  const rows2 = await connection.execute<UserRow[], [number]>('SELECT * FROM users WHERE id = ?', [1]);
  console.log(rows2[0].id);

  // batch with typed values
  type InsertParams = [number, string][];
  const batchRes = await connection.batch<UpsertResult, InsertParams>('INSERT INTO users VALUE (?,?)', [
    [1, 'a@b.com'],
    [2, 'c@d.com']
  ]);
  console.log(batchRes.affectedRows);

  // prepare with typed values
  type PrepareParams = [string, Buffer, Buffer];
  const stmt = await connection.prepare<PrepareParams>('INSERT INTO users VALUES (?, ?, ?)');
  console.log(stmt.id);
  await stmt.execute<UpsertResult>(['email', Buffer.from('hash'), Buffer.from('salt')]);
  const stream = stmt.executeStream(['email', Buffer.from('hash'), Buffer.from('salt')]);
  stream.on('data', (row: unknown) => console.log(row));
  stmt.close();

  // queryStream with typed values
  connection.queryStream<[number]>('SELECT * FROM users WHERE id = ?', [1]);

  // backward compat: no type argument still works (defaults to any)
  await connection.query('SELECT 1');
  await connection.execute('SELECT 1');
  await connection.batch('INSERT INTO myTable VALUE (?)', [[1], [2]]);
  const stmtAny = await connection.prepare('SELECT ?');
  await stmtAny.execute([1]);
  stmtAny.close();

  await connection.end();
}

async function testTypedValuesPool(): Promise<void> {
  const pool = newPool({ connectionLimit: 1 });

  type UserRow = { id: number; email: string };
  const rows = await pool.query<UserRow[], [number]>('SELECT * FROM users WHERE id = ?', [1]);
  console.log(rows[0].id);

  const rows2 = await pool.execute<UserRow[], [number]>('SELECT * FROM users WHERE id = ?', [1]);
  console.log(rows2[0].id);

  type InsertParams = [number, string][];
  const batchRes = await pool.batch<UpsertResult, InsertParams>('INSERT INTO users VALUE (?,?)', [
    [1, 'a@b.com'],
    [2, 'c@d.com']
  ]);
  console.log(batchRes.affectedRows);

  await pool.end();
}

async function runTests(): Promise<void> {
  try {
    await importSqlFile();
    await testMisc();
    await testChangeUser();
    await testTypeCast();
    await testPool();
    await testPoolCluster();
    await testRowsAsArray();
    await metaAsArray();
    await metaTypeHelpers();
    await testTypedValues();
    await testTypedValuesPool();

    console.log('done');
  } catch (err) {
    console.log('Unexpected error');
    console.log(err);
  } finally {
    process.exit();
  }
}

runTests();

console.log(version);
