import { main } from '../../bin/main.js';
import { htmlIsEqual } from '@markedjs/testutils';
import { resolve } from 'node:path';
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';

const execAsync = promisify(exec);

function createMocks() {
  const mocks = {
    stdout: '',
    stderr: '',
    code: null,
    stdin: {
      data: null,
      error: null,
      end: null,
    },
    process: {
      cwd: mock.fn(() => '/cwd'),
      env: [],
      argv: [],
      stdout: {
        write: mock.fn((str) => { mocks.stdout += str; }),
      },
      stderr: {
        write: mock.fn((str) => { mocks.stderr += str; }),
      },
      stdin: {
        setEncoding: mock.fn(),
        on: mock.fn((method, func) => {
          mocks.stdin[method] = func;
        }),
        resume: mock.fn(),
      },
      exit: mock.fn((code) => { mocks.code = code; }),
    },
  };

  return mocks;
}

function testInput({ args = [], stdin = '', stdinError = '', stdout = '', stderr = '', code = 0 } = {}) {
  return async() => {
    const mocks = createMocks();
    mocks.process.argv = ['node', 'marked', ...args];
    const mainPromise = main(mocks.process);
    if (typeof mocks.stdin.end === 'function') {
      if (stdin) {
        mocks.stdin.data(stdin);
      }
      if (stdinError) {
        mocks.stdin.error(stdinError);
      }
      mocks.stdin.end();
    }
    await mainPromise;

    assert.ok(await htmlIsEqual(mocks.stdout, stdout));
    assert.strictEqual(mocks.stderr, stderr);
    assert.strictEqual(mocks.code, code);
  };
}

function fixturePath(filePath) {
  return resolve(import.meta.dirname, './fixtures', filePath);
}

function execMarked({ args = '', stdin = '', stdout = '', stderr = '' }) {
  const execPath = resolve(import.meta.dirname, '../../bin/marked');
  return async() => {
    let command = '';

    if (stdin) {
      command += `node -e "process.stdout.write('${stdin}')" | `;
    }
    command += `node ${execPath}`;
    if (args) {
      command += ` ${args}`;
    }
    const res = await execAsync(command);

    assert.ok(await htmlIsEqual(res.stdout, stdout));
    assert.strictEqual(res.stderr, stderr);
  };
}

describe('bin/marked', () => {
  describe('string', () => {
    it('-s', testInput({
      args: ['-s', '# test'],
      stdout: '<h1>test</h1>',
    }));

    it('--string', testInput({
      args: ['--string', '# test'],
      stdout: '<h1>test</h1>',
    }));
  });

  describe('config', () => {
    it('-c', testInput({
      args: ['-c', fixturePath('bin-config.js'), '-s', 'line1\nline2'],
      stdout: '<p>line1<br>line2</p>',
    }));

    it('--config', testInput({
      args: ['--config', fixturePath('bin-config.js'), '-s', 'line1\nline2'],
      stdout: '<p>line1<br>line2</p>',
    }));

    it('config path with hash', testInput({
      args: ['--config', fixturePath('bin-config#hash.js'), '-s', 'line1\nline2'],
      stdout: '<p>line1<br>line2</p>',
    }));

    it('config not found', testInput({
      args: ['--config', fixturePath('does-not-exist.js'), '-s', 'line1\nline2'],
      stderr: `Cannot load config file '${fixturePath('does-not-exist.js')}'`,
      code: 1,
    }));
  });

  describe('input', () => {
    it('input file positional', testInput({
      args: [fixturePath('bin-input.md')],
      stdout: '<h1>file</h1>',
    }));

    it('input file not found', testInput({
      args: [fixturePath('does-not-exist.md')],
      stderr: `marked: ${fixturePath('does-not-exist.md')}: No such file or directory`,
      code: 1,
    }));

    it('input file not found --input', testInput({
      args: ['--input', fixturePath('does-not-exist.md')],
      stderr: `marked: ${fixturePath('does-not-exist.md')}: No such file or directory`,
      code: 1,
    }));

    it('reads from stdin when no files are provided', testInput({
      stdin: '# test\n',
      stdout: '<h1>test</h1>',
    }));
  });
});

describe('exec', () => {
  it('stdin', execMarked({
    stdin: '# test',
    stdout: '<h1>test</h1>',
  }));

  it('string', execMarked({
    args: '-s "# test"',
    stdout: '<h1>test</h1>',
  }));

  it('input', execMarked({
    args: `-i ${fixturePath('bin-input.md')}`,
    stdout: '<h1>file</h1>',
  }));

  it('input file positional', execMarked({
    args: fixturePath('bin-input.md'),
    stdout: '<h1>file</h1>',
  }));

  it('input last file positional', execMarked({
    args: `${fixturePath('does-not-exist.md')} ${fixturePath('bin-input.md')}`,
    stdout: '<h1>file</h1>',
  }));
});
