const assert = require('assert');
const childProcess = require('child_process');
const fs = require('fs');
const nodeGypBuild = require('node-gyp-build');
const os = require('os');
const path = require('path');
const util = require('util');

const OpenCC = require('./opencc');
const { prepareArtifacts } = require('../scripts/prepare-node-prebuild-artifacts');

const cases = JSON.parse(fs.readFileSync('test/testcases/testcases.json', 'utf-8')).cases || [];

function createLocalInstalledShape() {
  const rootDir = path.resolve(__dirname, '..');
  const jiebaPackageDir = path.join(rootDir, 'plugins', 'jieba', 'node');
  const libName = os.platform() === 'win32' ? 'opencc-jieba.dll'
    : os.platform() === 'darwin' ? 'libopencc-jieba.dylib'
      : 'libopencc-jieba.so';
  const requiredFiles = [
    path.join(jiebaPackageDir, 'index.js'),
    path.join(jiebaPackageDir, 'data', 's2twp_jieba.json'),
    path.join(jiebaPackageDir, 'data', 'jieba_dict', 'jieba_merged.ocd2'),
    path.join(jiebaPackageDir, 'prebuilds', `${os.platform()}-${os.arch()}`, libName),
  ];
  if (!requiredFiles.every((file) => fs.existsSync(file))) {
    return null;
  }
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-jieba-'));
  const nodeModulesDir = path.join(root, 'node_modules');
  fs.mkdirSync(nodeModulesDir, { recursive: true });
  fs.symlinkSync(rootDir, path.join(nodeModulesDir, 'opencc'), 'dir');
  fs.symlinkSync(jiebaPackageDir, path.join(nodeModulesDir, 'opencc-jieba'), 'dir');
  return root;
}

const testSync = function (tc, cfg, expected, done) {
  const opencc = new OpenCC(cfg + '.json');
  const converted = opencc.convertSync(tc.input);
  assert.equal(converted, expected);
  done();
};

const testAsync = function (tc, cfg, expected, done) {
  const opencc = new OpenCC(cfg + '.json');
  opencc.convert(tc.input, function (err, converted) {
    if (err) return done(err);
    assert.equal(converted, expected);
    done();
  });
};

async function testAsyncPromise(tc, cfg, expected) {
  const opencc = new OpenCC(cfg + '.json');
  const converted = await opencc.convertPromise(tc.input);
  assert.equal(converted, expected);
}

describe('Sync API', function () {
  cases.forEach(function (tc, idx) {
    Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) {
      it('[' + cfg + '] case #' + (idx + 1), function (done) {
        testSync(tc, cfg, expected, done);
      });
    });
  });
});

describe('Async API', function () {
  cases.forEach(function (tc, idx) {
    Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) {
      it('[' + cfg + '] case #' + (idx + 1), function (done) {
        testAsync(tc, cfg, expected, done);
      });
    });
  });
});

describe('Async Promise API', function () {
  cases.forEach(function (tc, idx) {
    Object.entries(tc.expected || {}).forEach(function ([cfg, expected]) {
      it('[' + cfg + '] case #' + (idx + 1), function (done) {
        testAsyncPromise(tc, cfg, expected).then(() => done(), done);
      });
    });
  });
});

describe('npm CLI', function () {
  const cli = path.join(__dirname, 'cli.js');

  function getAssetsPath() {
    const bindingPath = nodeGypBuild.path(path.join(__dirname, '..'));
    const bindingDir = path.dirname(bindingPath);
    const prebuildsDir = path.dirname(bindingDir);
    if (path.basename(prebuildsDir) === 'prebuilds') {
      const sharedAssetsPath = path.join(prebuildsDir, 'assets');
      if (fs.existsSync(sharedAssetsPath)) {
        return sharedAssetsPath;
      }
    }
    return bindingDir;
  }

  it('converts stdin to stdout', function () {
    const result = childProcess.spawnSync(process.execPath, [cli, '-c', 's2t.json'], {
      input: '汉字',
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout, '漢字');
  });

  it('appends .json to built-in config names', function () {
    const result = childProcess.spawnSync(process.execPath, [cli, '-c', 's2t'], {
      input: '汉字',
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout, '漢字');
  });

  it('converts input file to output file', function () {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-cli-'));
    const input = path.join(dir, 'input.txt');
    const output = path.join(dir, 'output.txt');
    fs.writeFileSync(input, '汉字', 'utf8');
    const result = childProcess.spawnSync(process.execPath, [
      cli,
      '--config=s2t.json',
      '--input',
      input,
      '--output',
      output,
    ], {
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(fs.readFileSync(output, 'utf8'), '漢字');
  });

  it('resolves custom relative config paths from cwd', function () {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-cli-config-'));
    const assetsPath = getAssetsPath();
    fs.copyFileSync(path.join(assetsPath, 's2t.json'), path.join(dir, 'custom-s2t.json'));
    fs.copyFileSync(path.join(assetsPath, 'STPhrases.ocd2'), path.join(dir, 'STPhrases.ocd2'));
    fs.copyFileSync(path.join(assetsPath, 'STCharacters.ocd2'), path.join(dir, 'STCharacters.ocd2'));

    const result = childProcess.spawnSync(process.execPath, [cli, '-c', './custom-s2t.json'], {
      cwd: dir,
      input: '汉字',
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout, '漢字');
  });

  it('does not append .json to custom relative config paths', function () {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-cli-config-stem-'));
    const assetsPath = getAssetsPath();
    fs.copyFileSync(path.join(assetsPath, 's2t.json'), path.join(dir, 'custom-s2t.json'));
    fs.copyFileSync(path.join(assetsPath, 'STPhrases.ocd2'), path.join(dir, 'STPhrases.ocd2'));
    fs.copyFileSync(path.join(assetsPath, 'STCharacters.ocd2'), path.join(dir, 'STCharacters.ocd2'));

    const result = childProcess.spawnSync(process.execPath, [cli, '-c', './custom-s2t'], {
      cwd: dir,
      input: '汉字',
      encoding: 'utf8',
    });
    assert.notEqual(result.status, 0);
    assert.match(result.stderr, /custom-s2t/);
  });

  it('prints help', function () {
    const result = childProcess.spawnSync(process.execPath, [cli, '--help'], {
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.match(result.stdout, /Usage:/);
    assert.match(result.stdout, /Unsupported in the npm CLI:/);
    assert.match(result.stdout, /--inspect/);
    assert.match(result.stdout, /--segmentation/);
  });

  it('prints version', function () {
    const result = childProcess.spawnSync(process.execPath, [cli, '--version'], {
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout.trim(), OpenCC.version);
  });

  it('rejects unsupported diagnostic modes', function () {
    const inspect = childProcess.spawnSync(process.execPath, [cli, '--inspect'], {
      encoding: 'utf8',
    });
    assert.notEqual(inspect.status, 0);
    assert.match(inspect.stderr, /not supported/);

    const segmentation = childProcess.spawnSync(process.execPath, [cli, '--segmentation'], {
      encoding: 'utf8',
    });
    assert.notEqual(segmentation.status, 0);
    assert.match(segmentation.stderr, /not supported/);
  });

  it('rejects empty inline option values', function () {
    for (const option of ['--config=', '--input=', '--output=']) {
      const result = childProcess.spawnSync(process.execPath, [cli, option], {
        input: '汉字',
        encoding: 'utf8',
      });
      assert.notEqual(result.status, 0);
      assert.match(result.stderr, /Missing value/);
    }
  });
});

describe('Optional opencc-jieba package integration', function () {
  it('loads jieba configs by mode name in the JavaScript API', function () {
    const installRoot = createLocalInstalledShape();
    if (!installRoot) this.skip();

    const script = [
      "const OpenCC = require('opencc');",
      "const converter = new OpenCC('s2twp_jieba');",
      "process.stdout.write(converter.convertSync('云计算'));",
    ].join('');
    const result = childProcess.spawnSync(process.execPath, ['-e', script], {
      cwd: installRoot,
      env: { ...process.env },
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout, '雲端計算');
  });

  it('loads jieba configs by mode name in the npm CLI', function () {
    const installRoot = createLocalInstalledShape();
    if (!installRoot) this.skip();

    const result = childProcess.spawnSync(process.execPath, [
      path.join(installRoot, 'node_modules', 'opencc', 'node', 'cli.js'),
      '-c',
      's2twp_jieba',
    ], {
      cwd: installRoot,
      env: { ...process.env },
      input: '云计算',
      encoding: 'utf8',
    });
    assert.equal(result.status, 0, result.stderr);
    assert.equal(result.stdout, '雲端計算');
  });
});

describe('Node prebuild assets', function () {
  it('collects only runtime json and ocd2 assets', function () {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-prebuild-assets-'));
    const releaseDir = path.join(root, 'build', 'Release');
    const assetsDir = path.join(root, 'prebuilds', 'assets');

    fs.mkdirSync(releaseDir, { recursive: true });
    fs.mkdirSync(assetsDir, { recursive: true });
    fs.writeFileSync(path.join(releaseDir, 's2t.json'), '{}');
    fs.writeFileSync(path.join(releaseDir, 'STCharacters.ocd2'), 'dict');
    fs.writeFileSync(path.join(releaseDir, 'STCharacters.txt'), 'source');
    fs.writeFileSync(path.join(releaseDir, 'README.md'), 'docs');
    fs.writeFileSync(path.join(assetsDir, 'stale.txt'), 'stale');

    prepareArtifacts(root);

    assert.deepEqual(fs.readdirSync(assetsDir).sort(), [
      'STCharacters.ocd2',
      's2t.json',
    ]);
  });
});
