// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict'

const assert = require('node:assert'),
  fs = require('node:fs'),
  path = require('node:path')

const io = require('selenium-webdriver/io')

describe('io', function () {
  describe('copy', function () {
    let tmpDir

    before(function () {
      return io.tmpDir().then(function (d) {
        tmpDir = d

        fs.writeFileSync(path.join(d, 'foo'), 'Hello, world')
      })
    })

    it('can copy one file to another', function () {
      return io.tmpFile().then(function (f) {
        return io.copy(path.join(tmpDir, 'foo'), f).then(function (p) {
          assert.strictEqual(p, f)
          assert.strictEqual('Hello, world', fs.readFileSync(p, 'utf-8'))
        })
      })
    })

    it('can copy symlink to destination', function () {
      if (process.platform === 'win32') {
        return // No symlinks on windows.
      }
      fs.symlinkSync(path.join(tmpDir, 'foo'), path.join(tmpDir, 'symlinked-foo'))
      return io.tmpFile().then(function (f) {
        return io.copy(path.join(tmpDir, 'symlinked-foo'), f).then(function (p) {
          assert.strictEqual(p, f)
          assert.strictEqual('Hello, world', fs.readFileSync(p, 'utf-8'))
        })
      })
    })

    it('fails if given a directory as a source', function () {
      return io
        .tmpFile()
        .then(function (f) {
          return io.copy(tmpDir, f)
        })
        .then(
          function () {
            throw Error('Should have failed with a type error')
          },
          function () {
            // Do nothing; expected.
          },
        )
    })
  })

  describe('copyDir', function () {
    it('copies recursively', function () {
      return io.tmpDir().then(function (dir) {
        fs.writeFileSync(path.join(dir, 'file1'), 'hello')
        fs.mkdirSync(path.join(dir, 'sub'))
        fs.mkdirSync(path.join(dir, 'sub/folder'))
        fs.writeFileSync(path.join(dir, 'sub/folder/file2'), 'goodbye')

        return io.tmpDir().then(function (dst) {
          return io.copyDir(dir, dst).then(function (ret) {
            assert.strictEqual(dst, ret)

            assert.strictEqual('hello', fs.readFileSync(path.join(dst, 'file1'), 'utf-8'))
            assert.strictEqual('goodbye', fs.readFileSync(path.join(dst, 'sub/folder/file2'), 'utf-8'))
          })
        })
      })
    })

    it('creates destination dir if necessary', function () {
      return io
        .tmpDir()
        .then(function (srcDir) {
          fs.writeFileSync(path.join(srcDir, 'foo'), 'hi')
          return io.tmpDir().then(function (dstDir) {
            return io.copyDir(srcDir, path.join(dstDir, 'sub'))
          })
        })
        .then(function (p) {
          assert.strictEqual('sub', path.basename(p))
          assert.strictEqual('hi', fs.readFileSync(path.join(p, 'foo'), 'utf-8'))
        })
    })

    it('supports regex exclusion filter', function () {
      return io
        .tmpDir()
        .then(function (src) {
          fs.writeFileSync(path.join(src, 'foo'), 'a')
          fs.writeFileSync(path.join(src, 'bar'), 'b')
          fs.writeFileSync(path.join(src, 'baz'), 'c')
          fs.mkdirSync(path.join(src, 'sub'))
          fs.writeFileSync(path.join(src, 'sub/quux'), 'd')
          fs.writeFileSync(path.join(src, 'sub/quot'), 'e')

          return io.tmpDir().then(function (dst) {
            return io.copyDir(src, dst, /(bar|quux)/)
          })
        })
        .then(function (dir) {
          assert.strictEqual('a', fs.readFileSync(path.join(dir, 'foo'), 'utf-8'))
          assert.strictEqual('c', fs.readFileSync(path.join(dir, 'baz'), 'utf-8'))
          assert.strictEqual('e', fs.readFileSync(path.join(dir, 'sub/quot'), 'utf-8'))

          assert.ok(!fs.existsSync(path.join(dir, 'bar')))
          assert.ok(!fs.existsSync(path.join(dir, 'sub/quux')))
        })
    })

    it('supports exclusion filter function', function () {
      return io
        .tmpDir()
        .then(function (src) {
          fs.writeFileSync(path.join(src, 'foo'), 'a')
          fs.writeFileSync(path.join(src, 'bar'), 'b')
          fs.writeFileSync(path.join(src, 'baz'), 'c')
          fs.mkdirSync(path.join(src, 'sub'))
          fs.writeFileSync(path.join(src, 'sub/quux'), 'd')
          fs.writeFileSync(path.join(src, 'sub/quot'), 'e')

          return io.tmpDir().then(function (dst) {
            return io.copyDir(src, dst, function (f) {
              return f !== path.join(src, 'foo') && f !== path.join(src, 'sub/quot')
            })
          })
        })
        .then(function (dir) {
          assert.strictEqual('b', fs.readFileSync(path.join(dir, 'bar'), 'utf-8'))
          assert.strictEqual('c', fs.readFileSync(path.join(dir, 'baz'), 'utf-8'))
          assert.strictEqual('d', fs.readFileSync(path.join(dir, 'sub/quux'), 'utf-8'))

          assert.ok(!fs.existsSync(path.join(dir, 'foo')))
          assert.ok(!fs.existsSync(path.join(dir, 'sub/quot')))
        })
    })
  })

  describe('exists', function () {
    var dir

    before(function () {
      return io.tmpDir().then(function (d) {
        dir = d
      })
    })

    it('returns a rejected promise if input value is invalid', function () {
      return io.exists(undefined).then(
        () => assert.fail('should have failed'),
        (e) => assert.ok(e instanceof TypeError),
      )
    })

    it('works for directories', function () {
      return io.exists(dir).then(assert.ok)
    })

    it('works for files', function () {
      var file = path.join(dir, 'foo')
      fs.writeFileSync(file, '')
      return io.exists(file).then(assert.ok)
    })

    it('does not return a rejected promise if file does not exist', function () {
      return io.exists(path.join(dir, 'not-there')).then(function (exists) {
        assert.ok(!exists)
      })
    })
  })

  describe('unlink', function () {
    let dir

    before(function () {
      return io.tmpDir().then(function (d) {
        dir = d
      })
    })

    it('silently succeeds if the path does not exist', function () {
      return io.unlink(path.join(dir, 'not-there'))
    })

    it('deletes files', function () {
      const file = path.join(dir, 'foo')
      fs.writeFileSync(file, '')
      return io
        .exists(file)
        .then(assert.ok)
        .then(function () {
          return io.unlink(file)
        })
        .then(function () {
          return io.exists(file)
        })
        .then(function (exists) {
          return assert.ok(!exists)
        })
    })
  })

  describe('rmDir', function () {
    it('succeeds if the designated directory does not exist', function () {
      return io.tmpDir().then(function (d) {
        return io.rmDir(path.join(d, 'i/do/not/exist'))
      })
    })

    it('deletes recursively', function () {
      return io.tmpDir().then(function (dir) {
        fs.writeFileSync(path.join(dir, 'file1'), 'hello')
        fs.mkdirSync(path.join(dir, 'sub'))
        fs.mkdirSync(path.join(dir, 'sub/folder'))
        fs.writeFileSync(path.join(dir, 'sub/folder/file2'), 'goodbye')

        return io.rmDir(dir).then(function () {
          assert.ok(!fs.existsSync(dir))
          assert.ok(!fs.existsSync(path.join(dir, 'sub/folder/file2')))
        })
      })
    })
  })

  describe('findInPath', function () {
    const savedPathEnv = process.env['PATH']
    afterEach(() => (process.env['PATH'] = savedPathEnv))

    const cwd = process.cwd
    afterEach(() => (process.cwd = cwd))

    let dirs
    beforeEach(() => {
      return Promise.all([io.tmpDir(), io.tmpDir(), io.tmpDir()]).then((arr) => {
        dirs = arr
        process.env['PATH'] = arr.join(path.delimiter)
      })
    })

    it('returns null if file cannot be found', () => {
      assert.strictEqual(io.findInPath('foo.txt'), null)
    })

    it('can find file on path', () => {
      let filePath = path.join(dirs[1], 'foo.txt')
      fs.writeFileSync(filePath, 'hi')

      assert.strictEqual(io.findInPath('foo.txt'), filePath)
    })

    it('returns null if file is in a subdir of a directory on the path', () => {
      let subDir = path.join(dirs[2], 'sub')
      fs.mkdirSync(subDir)

      let filePath = path.join(subDir, 'foo.txt')
      fs.writeFileSync(filePath, 'hi')

      assert.strictEqual(io.findInPath('foo.txt'), null)
    })

    it('does not match on directories', () => {
      fs.mkdirSync(path.join(dirs[2], 'sub'))
      assert.strictEqual(io.findInPath('sub'), null)
    })

    it('will look in cwd first if requested', () => {
      return io.tmpDir().then((fakeCwd) => {
        process.cwd = () => fakeCwd

        let theFile = path.join(fakeCwd, 'foo.txt')

        fs.writeFileSync(path.join(dirs[1], 'foo.txt'), 'hi')
        fs.writeFileSync(theFile, 'bye')

        assert.strictEqual(io.findInPath('foo.txt', true), theFile)
      })
    })
  })

  describe('read', function () {
    let tmpDir

    before(function () {
      return io.tmpDir().then(function (d) {
        tmpDir = d

        fs.writeFileSync(path.join(d, 'foo'), 'Hello, world')
      })
    })

    it('can read a file', function () {
      return io.read(path.join(tmpDir, 'foo')).then((buff) => {
        assert.ok(buff instanceof Buffer)
        assert.strictEqual('Hello, world', buff.toString())
      })
    })

    it('catches errors from invalid input', function () {
      return io.read({}).then(
        () => assert.fail('should have failed'),
        (e) => assert.ok(e instanceof TypeError),
      )
    })

    it('rejects returned promise if file does not exist', function () {
      return io.read(path.join(tmpDir, 'not-there')).then(
        () => assert.fail('should have failed'),
        (e) => assert.strictEqual('ENOENT', e.code),
      )
    })
  })

  describe('mkdirp', function () {
    it('recursively creates entire directory path', function () {
      return io.tmpDir().then((root) => {
        let dst = path.join(root, 'foo/bar/baz')
        return io.mkdirp(dst).then((d) => {
          assert.strictEqual(d, dst)
          return io.stat(d).then((stats) => {
            assert.ok(stats.isDirectory())
          })
        })
      })
    })

    it('does nothing if the directory already exists', function () {
      return io.tmpDir().then((dir) => io.mkdirp(dir).then((d) => assert.strictEqual(d, dir)))
    })
  })

  describe('walkDir', function () {
    it('walk directory', async function () {
      let dir = await io.tmpDir()

      fs.writeFileSync(path.join(dir, 'file1'), 'hello')
      fs.mkdirSync(path.join(dir, 'sub'))
      fs.mkdirSync(path.join(dir, 'sub/folder'))
      fs.writeFileSync(path.join(dir, 'sub/folder/file2'), 'goodbye')

      let seen = await io.walkDir(dir)
      seen.sort((a, b) => {
        if (a.path < b.path) return -1
        if (a.path > b.path) return 1
        return 0
      })
      assert.deepStrictEqual(seen, [
        { path: 'file1', dir: false },
        { path: 'sub', dir: true },
        { path: path.join('sub', 'folder'), dir: true },
        { path: path.join('sub', 'folder', 'file2'), dir: false },
      ])
    })
  })
})
