'use strict'
const t = require('tap')
const mkdirp = require('mkdirp')

// make our tests verify that windows link targets get turned into / paths
const fs = require('fs')
const { readlink, readlinkSync } = fs
fs.readlink = (path, cb) => {
  readlink(path, (er, path) => {
    if (er) {
      return cb(er)
    } else {
      cb(null, path.replace(/\//g, '\\'))
    }
  })
}
fs.readlinkSync = path => readlinkSync(path).replace(/\//g, '\\')

const ReadEntry = require('../lib/read-entry.js')
const makeTar = require('./make-tar.js')
const WriteEntry = require('../lib/write-entry.js')
const path = require('path')
const fixtures = path.resolve(__dirname, 'fixtures')
const files = path.resolve(fixtures, 'files')
const Header = require('../lib/header.js')
const mutateFS = require('mutate-fs')
process.env.USER = 'isaacs'
const chmodr = require('chmodr')
const Parser = require('../lib/parse.js')
const rimraf = require('rimraf')
const isWindows = process.platform === 'win32'
const normPath = require('../lib/normalize-windows-path.js')

t.test('set up', t => {
  const one = fs.statSync(files + '/hardlink-1')
  const two = fs.statSync(files + '/hardlink-2')
  if (one.dev !== two.dev || one.ino !== two.ino) {
    fs.unlinkSync(files + '/hardlink-2')
    fs.linkSync(files + '/hardlink-1', files + '/hardlink-2')
  }
  chmodr.sync(files, isWindows ? 0o666 : 0o644)
  t.end()
})

t.test('100 byte filename', t => {
  // do this one twice, so we have it with and without cache
  let statCache = null
  let linkCache = null
  t.plan(2)

  const runTest = t => {
    const f = '100-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
    const ws = new WriteEntry(f, {
      cwd: files,
      linkCache: linkCache,
      statCache: statCache,
    })

    let out = []
    ws.on('data', c => out.push(c))
    ws.on('end', _ => {
      out = Buffer.concat(out)
      t.match(ws, {
        header: {
          cksumValid: true,
          needPax: false,
          path: '100-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
          mode: isWindows ? 0o666 : 0o644,
          size: 100,
          linkpath: null,
          uname: 'isaacs',
          gname: null,
          devmaj: 0,
          devmin: 0,
        },
      })

      const wss = new WriteEntry.Sync(f, {
        cwd: files,
        linkCache: linkCache,
        statCache: statCache,
      })
      linkCache = ws.linkCache
      statCache = ws.statCache

      t.equal(out.slice(512).toString('hex'),
        wss.read().slice(512).toString('hex'))

      t.equal(out.length, 1024)
      t.equal(out.slice(0, 100).toString(), f)
      const h = new Header(out.slice(0, 512))
      t.match(h, {
        cksumValid: true,
        needPax: false,
        path: '100-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
        mode: isWindows ? 0o666 : 0o644,
        size: 100,
        linkpath: '',
        uname: 'isaacs',
        gname: '',
        devmaj: 0,
        devmin: 0,
      })

      t.equal(out.slice(512).toString('hex'),
        '6363636363636363636363636363636363636363636363636363636363636363' +
        '6363636363636363636363636363636363636363636363636363636363636363' +
        '6363636363636363636363636363636363636363636363636363636363636363' +
        '6363636300000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000' +
        '0000000000000000000000000000000000000000000000000000000000000000')

      t.end()
    })
  }

  t.test('uncached', runTest)
  t.test('cached', runTest)
})

t.test('directory', t => {
  const ws = new WriteEntry('dir', {
    cwd: files,
  })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.match(ws.header, {
      cksumValid: true,
      needPax: false,
      path: 'dir/',
      mode: isWindows ? 0o777 : 0o755,
      size: 0,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.equal(out.length, 512)

    const wss = new WriteEntry.Sync('dir', { cwd: files })
    t.equal(wss.read().length, 512)
    t.match(wss.header, {
      cksumValid: true,
      needPax: false,
      path: 'dir/',
      mode: isWindows ? 0o777 : 0o755,
      size: 0,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })

    t.end()
  })
})

t.test('empty path for cwd', t => {
  const ws = new WriteEntry('')
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.match(ws.header, {
      cksumValid: true,
      needPax: false,
      path: './',
      mode: isWindows ? 0o777 : fs.statSync('./').mode & 0o7777,
      size: 0,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.end()
  })
})

t.test('symlink', {
  skip: isWindows && 'symlinks not fully supported',
}, t => {
  const ws = new WriteEntry('symlink', { cwd: files })
  let out = []
  ws.on('data', c => out.push(c))
  const header = {
    cksumValid: true,
    needPax: false,
    path: 'symlink',
    size: 0,
    linkpath: 'hardlink-2',
    uname: 'isaacs',
    gname: null,
    devmaj: 0,
    devmin: 0,
  }

  const wss = new WriteEntry.Sync('symlink', { cwd: files })
  t.match(wss.header, header)

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.equal(out.length, 512)
    t.match(ws.header, header)
    t.end()
  })
})

t.test('zero-byte file', t => {
  const ws = new WriteEntry('files/zero-byte.txt', { cwd: fixtures })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.equal(out.length, 512)
    t.match(ws.header, {
      path: 'files/zero-byte.txt',
      cksumValid: true,
      needPax: false,
      mode: isWindows ? 0o666 : 0o644,
      size: 0,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.end()
  })
})

t.test('zero-byte file, but close fails', t => {
  const poop = new Error('poop')
  t.teardown(mutateFS.fail('close', poop))

  const ws = new WriteEntry('files/1024-bytes.txt', { cwd: fixtures })

  ws.on('end', _ =>
    t.fail('should not get an end, because the close fails'))

  ws.on('error', er => {
    t.match(er, { message: 'poop' })
    t.end()
  })
  ws.resume()
})

t.test('hardlinks', t => {
  const wss = new WriteEntry.Sync('hardlink-1', {
    cwd: files,
  })

  const ws = new WriteEntry('files/hardlink-2', {
    cwd: fixtures,
    linkCache: wss.linkCache,
  })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.equal(out.length, 512)
    t.match(ws.header, {
      type: 'Link',
      path: 'files/hardlink-2',
      cksumValid: true,
      needPax: false,
      mode: isWindows ? 0o666 : 0o644,
      size: 0,
      linkpath: 'files/hardlink-1',
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.end()
  })
})

t.test('hardlinks far away', t => {
  const h1 = 'hardlink-1'
  const f = path.resolve(files, h1)
  const stat = fs.statSync(f)
  const linkCache = new Map([[stat.dev + ':' + stat.ino, '/a/b/c/d/e']])

  const ws = new WriteEntry('files/hardlink-2', {
    cwd: fixtures,
    linkCache: linkCache,
  })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.equal(out.length, 1024)
    t.match(ws.header, {
      path: 'files/hardlink-2',
      cksumValid: true,
      needPax: false,
      mode: isWindows ? 0o666 : 0o644,
      size: 26,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.end()
  })
})

t.test('really deep path', t => {
  const f = 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
  const ws = new WriteEntry(f, { cwd: files })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.match(ws.header, {
      cksumValid: true,
      needPax: true,
      path: 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
      mode: isWindows ? 0o666 : 0o644,
      size: 100,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.equal(out.length, 2048)
    t.end()
  })
})

t.test('no pax', t => {
  const f = 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
  const ws = new WriteEntry(f, { cwd: files, noPax: true })
  let out = []
  ws.on('data', c => out.push(c))

  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.match(ws.header, {
      cksumValid: true,
      needPax: true,
      path: 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
      mode: isWindows ? 0o666 : 0o644,
      size: 100,
      linkpath: null,
      uname: 'isaacs',
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.equal(out.length, 1024)
    t.end()
  })
})

t.test('nonexistent file', t => {
  const f = path.resolve(files, 'does not exist')
  const ws = new WriteEntry('does not exist', { cwd: files })
  ws.on('error', er => {
    t.match(er, {
      message: 'ENOENT: no such file or directory, lstat \'' + f + '\'',
      code: 'ENOENT',
      path: f,
      syscall: 'lstat',
    })
    t.end()
  })
})

t.test('absolute path', t => {
  const absolute = path.resolve(files, '512-bytes.txt')
  const { root } = path.parse(absolute)
  const f = root + root + root + absolute
  const warn = normPath(isWindows ? root : root + root + root + root)
  t.test('preservePaths=false strict=false', t => {
    const warnings = []
    // on windows, c:\c:\c:\... is a valid path, so just use the
    // single-root absolute version of it.
    const ws = new WriteEntry(isWindows ? absolute : f, {
      cwd: files,
      onwarn: (c, m, p) => warnings.push([c, m, p]),
    })
    let out = []
    ws.on('data', c => out.push(c))
    ws.on('end', _ => {
      out = Buffer.concat(out)
      t.equal(out.length, 1024)
      t.match(warnings, [[
        'TAR_ENTRY_INFO',
        `stripping ${warn} from absolute path`,
        { path: normPath(isWindows ? absolute : f) },
      ]])

      t.match(ws.header, {
        cksumValid: true,
        needPax: false,
        path: normPath(absolute.replace(/^(\/|[a-z]:[/\\])*/i, '')),
        mode: isWindows ? 0o666 : 0o644,
        size: 512,
        linkpath: null,
        uname: 'isaacs',
        gname: null,
        devmaj: 0,
        devmin: 0,
      })
      t.end()
    })
  })

  t.test('preservePaths=true', t => {
    t.plan(2)
    // with preservePaths, strictness doens't matter
    ;[true, false].forEach(strict => {
      t.test('strict=' + strict, t => {
        const warnings = []
        const ws = new WriteEntry(isWindows ? absolute : f, {
          cwd: files,
          strict: strict,
          preservePaths: true,
          onwarn: (c, m, p) => warnings.push([c, m, p]),
        })
        let out = []
        ws.on('data', c => out.push(c))
        ws.on('end', _ => {
          out = Buffer.concat(out)
          t.equal(warnings.length, 0)

          t.match(ws.header, {
            cksumValid: true,
            needPax: false,
            path: normPath(isWindows ? absolute : f),
            mode: isWindows ? 0o666 : 0o644,
            size: 512,
            linkpath: null,
            uname: 'isaacs',
            gname: null,
            devmaj: 0,
            devmin: 0,
          })
          t.end()
        })
      })
    })
  })

  t.test('preservePaths=false strict=true', t => {
    t.throws(_ => {
      new WriteEntry(isWindows ? absolute : f, {
        strict: true,
        cwd: files,
      })
    }, {
      message: /stripping .* from absolute path/,
      path: normPath(isWindows ? absolute : f),
    })
    t.end()
  })

  t.end()
})

t.throws(_ => new WriteEntry(null), new TypeError('path is required'))

t.test('no user environ, sets uname to empty string', t => {
  delete process.env.USER
  const ws = new WriteEntry('512-bytes.txt', { cwd: files })
  let out = []
  ws.on('data', c => out.push(c))
  ws.on('end', _ => {
    out = Buffer.concat(out)
    t.equal(out.length, 1024)
    t.match(ws.header, {
      cksumValid: true,
      needPax: false,
      path: '512-bytes.txt',
      mode: isWindows ? 0o666 : 0o644,
      size: 512,
      uname: '',
      linkpath: null,
      gname: null,
      devmaj: 0,
      devmin: 0,
    })
    t.end()
  })
})

t.test('an unsuppored type', {
  skip: isWindows && '/dev/random on windows',
}, t => {
  const ws = new WriteEntry('/dev/random', { preservePaths: true })
  ws.on('data', c => {
    throw new Error('should not get data from random')
  })
  ws.on('stat', stat => {
    t.match(stat, {
      dev: Number,
      mode: 0o020666,
      nlink: 1,
      rdev: Number,
      blksize: Number,
      ino: Number,
      size: 0,
      blocks: 0,
    })
    t.ok(stat.isCharacterDevice(), 'random is a character device')
  })
  ws.on('end', _ => {
    t.match(ws, { type: 'Unsupported', path: '/dev/random' })
    t.end()
  })
})

t.test('readlink fail', t => {
  const expect = {
    syscall: 'readlink',
    path: String,
  }
  // pretend everything is a symbolic link, then read something that isn't
  t.teardown(mutateFS.statType('SymbolicLink'))
  t.throws(_ => {
    return new WriteEntry.Sync('write-entry.js', { cwd: __dirname })
  }, expect)
  new WriteEntry('write-entry.js', { cwd: __dirname }).on('error', er => {
    t.match(er, expect)
    t.equal(normPath(er.path), normPath(__filename))
    t.end()
  })
})

t.test('open fail', t => {
  t.teardown(mutateFS.fail('open', new Error('pwn')))
  t.throws(_ => new WriteEntry.Sync('write-entry.js', { cwd: __dirname }),
    { message: 'pwn' })
  new WriteEntry('write-entry.js', { cwd: __dirname }).on('error', er => {
    t.match(er, { message: 'pwn' })
    t.end()
  })
})

t.test('read fail', t => {
  const expect = {
    message: 'EISDIR: illegal operation on a directory, read',
    code: 'EISDIR',
    syscall: 'read',
  }
  // pretend everything is a file, then read something that isn't
  t.teardown(mutateFS.statMutate((er, st) => {
    if (er) {
      return [er, st]
    }
    st.isFile = () => true
    st.size = 123
  }))
  t.throws(_ => new WriteEntry.Sync('fixtures', {
    cwd: __dirname,
  }), expect)
  new WriteEntry('fixtures', { cwd: __dirname }).on('error', er => {
    t.match(er, expect)
    t.end()
  })
})

t.test('read invalid EOF', t => {
  t.teardown(mutateFS.mutate('read', (er, br) => [er, 0]))
  const expect = {
    message: 'encountered unexpected EOF',
    path: normPath(__filename),
    syscall: 'read',
    code: 'EOF',
  }
  t.throws(_ => new WriteEntry.Sync('write-entry.js', { cwd: __dirname }),
    expect)
  new WriteEntry('write-entry.js', { cwd: __dirname }).on('error', er => {
    t.match(er, expect)
    t.end()
  })
})

t.test('read overflow expectation', t => {
  t.teardown(mutateFS.statMutate((er, st) => {
    if (st) {
      st.size = 3
    }
  }))
  const f = '512-bytes.txt'
  const expect = {
    message: 'did not encounter expected EOF',
    path: normPath(path.resolve(files, f)),
    syscall: 'read',
    code: 'EOF',
  }
  t.plan(2)
  t.throws(_ => new WriteEntry.Sync(f, { cwd: files, maxReadSize: 2 }), expect)
  new WriteEntry(f, { cwd: files, maxReadSize: 2 }).on('error', er => {
    t.match(er, expect)
  }).resume()
})

t.test('short reads', t => {
  t.teardown(mutateFS.zenoRead())
  const cases = {
    '1024-bytes.txt': new Array(1024).join('x') + '\n',
    '100-byte-filename-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc': new Array(101).join('c'),
  }

  const maxReadSize = [null, 1024, 100, 111]

  Object.keys(cases).forEach(filename => {
    t.test(filename.split('byte').shift() + 'byte', t => {
      const contents = cases[filename]
      maxReadSize.forEach(mRS => {
        t.test('maxReadSize=' + mRS, t => {
          let out = []
          const ws = new WriteEntry(filename, {
            maxReadSize: mRS,
            cwd: files,
          })
          ws.on('data', c => out.push(c))
          ws.on('end', _ => {
            out = Buffer.concat(out)
            t.equal(out.length, 512 * Math.ceil(1 + contents.length / 512))
            t.equal(out.slice(512).toString().replace(/\0.*$/, ''), contents)
            const wss = new WriteEntry.Sync(filename, { cwd: files })
            const syncOut = wss.read()
            t.equal(syncOut.length, out.length)
            t.equal(syncOut.slice(512).toString(), out.slice(512).toString())
            t.end()
          })
        })
      })
      t.end()
    })
  })
  t.end()
})

t.test('win32 path conversion', {
  skip: isWindows && 'no need to test on windows',
}, t => {
  const ws = new WriteEntry('long-path\\r', {
    cwd: files,
    win32: true,
  })
  t.equal(ws.path, 'long-path/r')
  t.end()
})

t.test('win32 <|>? in paths', {
  skip: isWindows && 'do not create annoying junk on windows systems',
}, t => {
  const file = path.resolve(fixtures, '<|>?.txt')
  const uglyName = Buffer.from('ef80bcef81bcef80beef80bf2e747874', 'hex').toString()
  const ugly = path.resolve(fixtures, uglyName)
  t.teardown(_ => {
    rimraf.sync(file)
    rimraf.sync(ugly)
  })

  fs.writeFileSync(ugly, '<|>?')

  const wc = new WriteEntry(uglyName, {
    cwd: fixtures,
    win32: true,
  })

  const out = []
  wc.on('data', c => out.push(c))
  wc.on('end', _ => {
    const data = Buffer.concat(out).toString()
    t.equal(data.slice(0, 4), '<|>?')
    t.end()
  })

  t.equal(wc.path, '<|>?.txt')
  t.equal(wc.absolute, ugly)
})

t.test('uid doesnt match, dont set uname', t => {
  t.teardown(mutateFS.statMutate((er, st) => {
    if (st) {
      st.uid -= 1
    }
  }))
  const ws = new WriteEntry('long-path/r', {
    cwd: files,
  })
  t.notOk(ws.uname)
  t.end()
})

t.test('override absolute to some other file', t => {
  const ws = new WriteEntry('blerg', {
    absolute: files + '/one-byte.txt',
  })
  const out = []
  ws.on('data', c => out.push(c))
  ws.on('end', _ => {
    const data = Buffer.concat(out)
    t.equal(data.length, 1024)
    t.match(data.slice(512).toString(), /^a\0{511}$/)
    t.match(ws, {
      path: 'blerg',
      header: { size: 1 },
    })
    const wss = new WriteEntry.Sync('blerg', {
      absolute: files + '/one-byte.txt',
    })
    const sdata = wss.read()
    t.equal(sdata.length, 1024)
    t.match(sdata.slice(512).toString(), /^a\0{511}$/)
    t.match(wss, {
      path: 'blerg',
      header: { size: 1 },
    })
    t.end()
  })
})

t.test('portable entries, nothing platform-specific', t => {
  const om = 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt'
  const ws = new WriteEntry(om, {
    cwd: files,
    portable: true,
  })

  const pexpect = {
    atime: null,
    charset: null,
    comment: null,
    ctime: null,
    gid: null,
    gname: null,
    linkpath: null,
    path: 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt',
    size: 2,
    uid: null,
    uname: null,
    dev: null,
    ino: null,
    nlink: null,
  }

  const hexpect = {
    size: 2,
    ctime: null,
    atime: null,
    uid: null,
    uname: '',
    gid: null,
    gname: '',
  }

  const ps = new Parser()
  const wss = new WriteEntry.Sync(om, {
    cwd: files,
    portable: true,
  })
  ps.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
  })
  ps.end(wss.read())

  const p = new Parser()
  ws.pipe(p)
  p.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
    t.end()
  })
})

t.test('no mtime', t => {
  const om = 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt'
  const ws = new WriteEntry(om, {
    cwd: files,
    noMtime: true,
    portable: true,
  })

  const pexpect = {
    atime: null,
    mtime: null,
    charset: null,
    comment: null,
    ctime: null,
    gid: null,
    gname: null,
    linkpath: null,
    path: 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt',
    size: 2,
    uid: null,
    uname: null,
    dev: null,
    ino: null,
    nlink: null,
  }

  const hexpect = {
    size: 2,
    ctime: null,
    atime: null,
    mtime: null,
    uid: null,
    uname: '',
    gid: null,
    gname: '',
  }

  const ps = new Parser()
  const wss = new WriteEntry.Sync(om, {
    cwd: files,
    portable: true,
    noMtime: true,
  })
  ps.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
  })
  ps.end(wss.read())

  const p = new Parser()
  ws.pipe(p)
  p.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
    t.end()
  })
})

t.test('force mtime', t => {
  const om = 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt'
  const date = new Date('1979-07-01T19:10:00.000Z')
  const ws = new WriteEntry(om, {
    cwd: files,
    mtime: date,
    portable: true,
  })

  const pexpect = {
    atime: null,
    mtime: new Date('1979-07-01T19:10:00.000Z'),
    charset: null,
    comment: null,
    ctime: null,
    gid: null,
    gname: null,
    linkpath: null,
    path: 'long-path/r/e/a/l/l/y/-/d/e/e/p/-/f/o/l/d/e/r/-/p/a/t/h/Ω.txt',
    size: 2,
    uid: null,
    uname: null,
    dev: null,
    ino: null,
    nlink: null,
  }

  const hexpect = {
    size: 2,
    ctime: null,
    atime: null,
    mtime: new Date('1979-07-01T19:10:00.000Z'),
    uid: null,
    uname: '',
    gid: null,
    gname: '',
  }

  const ps = new Parser()
  const wss = new WriteEntry.Sync(om, {
    cwd: files,
    portable: true,
    mtime: new Date('1979-07-01T19:10:00.000Z'),
  })
  ps.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
  })
  ps.end(wss.read())

  const p = new Parser()
  ws.pipe(p)
  p.on('entry', entry => {
    t.match(entry.extended, pexpect)
    t.match(entry.header, hexpect)
    t.end()
  })
})

t.test('portable dir entries, no mtime', t => {
  const dir = 'long-path/'
  const ws = new WriteEntry(dir, {
    cwd: files,
    portable: true,
  })

  const hexpect = {
    path: 'long-path/',
    ctime: null,
    atime: null,
    uid: null,
    uname: '',
    gid: null,
    gname: '',
    mtime: null,
  }

  const ps = new Parser()
  const wss = new WriteEntry.Sync(dir, {
    cwd: files,
    portable: true,
  })
  ps.on('entry', entry => {
    t.match(entry.header, hexpect)
  })
  ps.end(wss.read())

  const p = new Parser()
  ws.pipe(p)
  p.on('entry', entry => {
    t.match(entry.header, hexpect)
    t.end()
  })
})

t.test('writing more data than is appropriate', t => {
  const path = t.testdir({
    file: 'hello',
  })
  const wss = new WriteEntry(`${path}/file`)
  wss.on('error', er => {
    t.match(er, {
      message: 'writing more data than expected',
    })
    t.end()
  })
  wss.on('stat', () => wss.write(Buffer.from('some more stuff')))
})

t.test('write entry from read entry', t => {
  const data = makeTar([
    {
      path: '$',
      type: 'File',
      size: 10,
      mode: isWindows ? 0o777 : 0o755,
      uid: 123,
      gid: 321,
      ctime: new Date('1979-07-01'),
      atime: new Date('1980-08-17'),
    },
    '$$$$$$$$$$',
    {
      path: 'blep',
      type: 'SymbolicLink',
      linkpath: 'xyz',
    },
    '',
    '',
  ])

  t.test('basic file', t => {
    const fileEntry = new ReadEntry(new Header(data))
    const wetFile = new WriteEntry.Tar(fileEntry)
    const out = []
    let wetFileEnded = false
    wetFile.on('data', c => out.push(c))
    wetFile.on('end', _ => wetFileEnded = true)
    fileEntry.write(data.slice(512, 550))
    fileEntry.write(data.slice(550, 1000))
    fileEntry.end(data.slice(1000, 1024))
    t.equal(wetFileEnded, true)
    const result = Buffer.concat(out)
    t.equal(result.length, 1024)
    t.equal(result.toString().replace(/\0.*$/, ''), '$')
    const body = result.slice(512).toString().replace(/\0*$/, '')
    t.equal(body, '$$$$$$$$$$')
    t.end()
  })

  t.test('basic file', t => {
    const data = makeTar([
      {
        path: '$',
        type: 'Directory',
        size: 0,
        mode: isWindows ? 0o777 : 0o755,
        uid: 123,
        gid: 321,
        ctime: new Date('1979-07-01'),
        atime: new Date('1980-08-17'),
        mtime: new Date('1997-06-21'),
      },
      '',
      '',
    ])
    const fileEntry = new ReadEntry(new Header(data))
    const wetFile = new WriteEntry.Tar(fileEntry, { portable: true })
    const out = []
    let wetFileEnded = false
    wetFile.on('data', c => out.push(c))
    wetFile.on('end', _ => wetFileEnded = true)
    fileEntry.end()
    t.equal(wetFileEnded, true)
    const result = Buffer.concat(out)
    t.match(new Header(result), { mtime: null })
    t.end()
  })

  t.test('with pax header', t => {
    const fileEntryPax = new ReadEntry(new Header(data))
    fileEntryPax.path = new Array(200).join('$')
    const wetPax = new WriteEntry.Tar(fileEntryPax)
    let wetPaxEnded = false
    const out = []
    wetPax.on('data', c => out.push(c))
    wetPax.on('end', _ => wetPaxEnded = true)
    fileEntryPax.write(data.slice(512, 550))
    fileEntryPax.write(data.slice(550, 1000))
    fileEntryPax.end(data.slice(1000, 1024))
    t.equal(wetPaxEnded, true)
    const result = Buffer.concat(out)
    t.equal(result.length, 2048)
    t.match(result.slice(1024, 1124).toString(), /^\$+\0?$/)
    const body = result.slice(1536).toString().replace(/\0*$/, '')
    t.match(new Header(result), { type: 'ExtendedHeader' })
    t.equal(body, '$$$$$$$$$$')
    t.end()
  })

  t.test('pax and portable', t => {
    const fileEntryPax = new ReadEntry(new Header(data))
    fileEntryPax.path = new Array(200).join('$')
    const wetPax = new WriteEntry.Tar(fileEntryPax, { portable: true })
    let wetPaxEnded = false
    const out = []
    wetPax.on('data', c => out.push(c))
    wetPax.on('end', _ => wetPaxEnded = true)
    fileEntryPax.write(data.slice(512, 550))
    fileEntryPax.write(data.slice(550, 1000))
    fileEntryPax.end(data.slice(1000, 1024))
    t.equal(wetPaxEnded, true)
    const result = Buffer.concat(out)
    t.equal(result.length, 2048)
    t.match(result.slice(1024, 1124).toString(), /^\$+\0?$/)
    t.match(new Header(result), { type: 'ExtendedHeader' })
    t.match(new Header(result.slice(1024)), {
      ctime: null,
      atime: null,
      uname: '',
      gname: '',
    })
    const body = result.slice(1536).toString().replace(/\0*$/, '')
    t.equal(body, '$$$$$$$$$$')
    t.end()
  })

  t.test('pax, portable, and noMtime', t => {
    const fileEntryPax = new ReadEntry(new Header(data))
    fileEntryPax.path = new Array(200).join('$')
    const wetPax = new WriteEntry.Tar(fileEntryPax, {
      noMtime: true,
      portable: true,
    })
    let wetPaxEnded = false
    const out = []
    wetPax.on('data', c => out.push(c))
    wetPax.on('end', _ => wetPaxEnded = true)
    fileEntryPax.write(data.slice(512, 550))
    fileEntryPax.write(data.slice(550, 1000))
    fileEntryPax.end(data.slice(1000, 1024))
    t.equal(wetPaxEnded, true)
    const result = Buffer.concat(out)
    t.equal(result.length, 2048)
    t.match(result.slice(1024, 1124).toString(), /^\$+\0?$/)
    t.match(new Header(result), { type: 'ExtendedHeader' })
    t.match(new Header(result.slice(1024)), {
      mtime: null,
      ctime: null,
      atime: null,
      uname: '',
      gname: '',
    })
    const body = result.slice(1536).toString().replace(/\0*$/, '')
    t.equal(body, '$$$$$$$$$$')
    t.end()
  })

  t.test('abs path', t => {
    const fileEntry = new ReadEntry(new Header(data))
    fileEntry.path = '/a/b/c'

    t.test('warn', t => {
      const warnings = []
      new WriteEntry.Tar(fileEntry, {
        onwarn: (code, msg, data) => warnings.push(code, msg, data),
      })
      t.match(warnings, ['TAR_ENTRY_INFO', 'stripping / from absolute path', {
        path: '/a/b/c',
      }])
      t.end()
    })

    t.test('preserve', t => {
      const warnings = []
      new WriteEntry.Tar(fileEntry, {
        onwarn: (code, msg, data) => warnings.push(code, msg, data),
        preservePaths: true,
      })
      t.same(warnings, [])
      t.end()
    })

    t.test('throw', t => {
      t.throws(_ => new WriteEntry.Tar(fileEntry, {
        strict: true,
      }))
      t.end()
    })
    t.end()
  })

  t.test('no block remain', t => {
    const readEntry = new ReadEntry(new Header({
      size: 512,
      type: 'File',
      path: 'x',
    }))
    const wet = new WriteEntry.Tar(readEntry)
    const out = []
    wet.on('data', c => out.push(c))
    let wetEnded = false
    wet.on('end', _ => wetEnded = true)
    t.equal(wetEnded, false)
    readEntry.end(Buffer.from(new Array(513).join('@')))
    t.equal(wetEnded, true)
    const res = Buffer.concat(out)
    t.equal(res.length, 1024)
    t.match(res.slice(512).toString(), /^@+$/)
    t.end()
  })

  t.test('write more than appropriate', t => {
    const readEntry = new ReadEntry(new Header({
      path: 'x',
      type: 'File',
      size: '1',
    }))
    const wet = new WriteEntry.Tar(readEntry)
    t.throws(_ => wet.write(Buffer.from(new Array(1024).join('x'))))
    t.end()
  })

  t.end()
})

// https://github.com/npm/node-tar/issues/284
t.test('prefix and hard links', t => {
  const dir = path.resolve(fixtures, 'writeentry-prefix-hardlinks')
  rimraf.sync(dir)
  t.teardown(_ => rimraf.sync(dir))
  mkdirp.sync(dir + '/in/z/b/c')
  fs.writeFileSync(dir + '/in/target', 'ddd')
  fs.linkSync(dir + '/in/target', dir + '/in/z/b/c/d')
  fs.linkSync(dir + '/in/target', dir + '/in/z/b/d')
  fs.linkSync(dir + '/in/target', dir + '/in/z/d')
  fs.linkSync(dir + '/in/target', dir + '/in/y')
  const long = 'y'.repeat(250)
  fs.linkSync(dir + '/in/target', dir + '/in/' + long)

  const expect = [
    {
      type: 'Directory',
      path: 'out/x/',
    },
    {
      type: 'File',
      size: 3,
      path: 'out/x/target',
    },
    'ddd\0',
    {
      path: 'out/x/y',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      path: 'PaxHeader/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'ExtendedHeader',
    },
    new RegExp('^266 path=out.x.' + long + '[\\w\\W]*linkpath=out.x.target'),
    {
      path: 'out/x/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      type: 'Directory',
      path: 'out/x/z/',
    },
    {
      type: 'Directory',
      path: 'out/x/z/b/',
    },
    {
      path: 'out/x/z/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      type: 'Directory',
      path: 'out/x/z/b/c/',
    },
    {
      path: 'out/x/z/b/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      path: 'out/x/z/b/c/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
  ]

  const check = (out, t) => {
    const data = Buffer.concat(out)
    expect.forEach((e, i) => {
      if (typeof e === 'string') {
        t.equal(data.slice(i * 512, i * 512 + e.length).toString(), e)
      } else if (e instanceof RegExp) {
        t.match(data.slice(i * 512, (i + 1) * 512).toString(), e)
      } else {
        t.match(new Header(data.slice(i * 512, (i + 1) * 512)), e)
      }
    })
  }

  const runTest = async (t, path, Class) => {
    const linkCache = new Map()
    const statCache = new Map()
    const opt = {
      cwd: dir + '/in',
      prefix: 'out/x',
      noDirRecurse: true,
      linkCache,
      statCache,
    }
    const out = []
    const entry = (path) => new Promise(resolve => {
      const p = new Class(path, opt)
      p.on('end', resolve)
      p.on('data', d => out.push(d))
    })

    await entry(path)
    if (path === '.') {
      path = './'
    }
    await entry(`${path}target`)
    await entry(`${path}y`)
    await entry(`${path}${long}`)
    await entry(`${path}z`)
    await entry(`${path}z/b`)
    await entry(`${path}z/d`)
    await entry(`${path}z/b/c`)
    await entry(`${path}z/b/d`)
    await entry(`${path}z/b/c/d`)
    check(out, t)
  }

  t.test('async', t => {
    t.test('.', t => runTest(t, '.', WriteEntry))
    return t.test('./', t => runTest(t, './', WriteEntry))
  })

  t.test('sync', t => {
    t.test('.', t => runTest(t, '.', WriteEntry.Sync))
    return t.test('./', t => runTest(t, './', WriteEntry.Sync))
  })

  t.end()
})

t.test('prefix and hard links from tar entries', t => {
  const long = 'y'.repeat(250)
  const expect = [
    {
      type: 'Directory',
      path: 'out/x/',
    },
    {
      type: 'File',
      size: 3,
      path: 'out/x/target',
    },
    'ddd\0',
    {
      path: 'out/x/y',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      path: 'PaxHeader/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'ExtendedHeader',
    },
    new RegExp('^266 path=out.x.' + long + '[\\w\\W]*linkpath=out.x.target'),
    {
      path: 'out/x/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      type: 'Directory',
      path: 'out/x/z/',
    },
    {
      type: 'Directory',
      path: 'out/x/z/b/',
    },
    {
      path: 'out/x/z/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      type: 'Directory',
      path: 'out/x/z/b/c/',
    },
    {
      path: 'out/x/z/b/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
    {
      path: 'out/x/z/b/c/d',
      type: 'Link',
      linkpath: 'out/x/target',
    },
  ]

  const data = makeTar([
    {
      type: 'Directory',
      path: './',
    },
    {
      type: 'File',
      size: 3,
      path: 'target',
    },
    'ddd\0',
    {
      path: 'y',
      type: 'Link',
      linkpath: 'target',
    },
    {
      path: 'PaxHeader/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'ExtendedHeader',
      size: ('260 path=' + long + '\n19 linkpath=target\n').length,
    },
    '260 path=' + long + '\n19 linkpath=target\n',
    {
      path: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/',
    },
    {
      type: 'Directory',
      path: 'z/b/',
    },
    {
      path: 'z/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/b/c/',
    },
    {
      path: 'z/b/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      path: 'z/b/c/d',
      type: 'Link',
      linkpath: 'target',
    },
    '',
    '',
  ])

  const check = (out, t) => {
    const data = Buffer.concat(out)
    expect.forEach((e, i) => {
      if (typeof e === 'string') {
        t.equal(data.slice(i * 512, i * 512 + e.length).toString(), e)
      } else if (e instanceof RegExp) {
        t.match(data.slice(i * 512, (i + 1) * 512).toString(), e)
      } else {
        t.match(new Header(data.slice(i * 512, (i + 1) * 512)), e)
      }
    })
  }

  const runTest = async (t, path) => {
    const linkCache = new Map()
    const statCache = new Map()
    const opt = {
      prefix: 'out/x',
      noDirRecurse: true,
      linkCache,
      statCache,
    }
    const out = []
    const parser = new Parser({
      strict: true,
      onentry: readEntry => {
        const p = new WriteEntry.Tar(readEntry, opt)
        p.on('data', d => out.push(d))
      },
    })
    parser.on('end', () => check(out, t))
    parser.end(data)
  }

  t.test('.', t => runTest(t, '.'))
  t.test('./', t => runTest(t, './'))

  t.end()
})

t.test('hard links and no prefix', t => {
  const dir = path.resolve(fixtures, 'writeentry-prefix-hardlinks')
  t.teardown(_ => rimraf.sync(dir))
  mkdirp.sync(dir + '/in/z/b/c')
  fs.writeFileSync(dir + '/in/target', 'ddd')
  fs.linkSync(dir + '/in/target', dir + '/in/z/b/c/d')
  fs.linkSync(dir + '/in/target', dir + '/in/z/b/d')
  fs.linkSync(dir + '/in/target', dir + '/in/z/d')
  fs.linkSync(dir + '/in/target', dir + '/in/y')

  const expect = [
    {
      type: 'Directory',
      path: './',
    },
    {
      type: 'File',
      size: 3,
      path: 'target',
    },
    'ddd\0',
    {
      path: 'y',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/',
    },
    {
      type: 'Directory',
      path: 'z/b/',
    },
    {
      path: 'z/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/b/c/',
    },
    {
      path: 'z/b/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      path: 'z/b/c/d',
      type: 'Link',
      linkpath: 'target',
    },
  ]

  const check = (out, t) => {
    const data = Buffer.concat(out)
    expect.forEach((e, i) => {
      if (typeof e === 'string') {
        t.equal(data.slice(i * 512, i * 512 + e.length).toString(), e)
      } else {
        t.match(new Header(data.slice(i * 512, (i + 1) * 512)), e)
      }
    })
  }

  const runTest = async (t, path, Class) => {
    const linkCache = new Map()
    const statCache = new Map()
    const opt = {
      cwd: dir + '/in',
      linkCache,
      statCache,
    }
    const out = []
    const entry = (path) => new Promise(resolve => {
      const p = new Class(path, opt)
      p.on('end', resolve)
      p.on('data', d => out.push(d))
    })

    await entry(path)
    if (path === '.') {
      path = './'
    }
    await entry(`${path}target`)
    await entry(`${path}y`)
    await entry(`${path}z`)
    await entry(`${path}z/b`)
    await entry(`${path}z/d`)
    await entry(`${path}z/b/c`)
    await entry(`${path}z/b/d`)
    await entry(`${path}z/b/c/d`)
    check(out, t)
  }

  t.test('async', t => {
    t.test('.', t => runTest(t, '.', WriteEntry))
    return t.test('./', t => runTest(t, './', WriteEntry))
  })

  t.test('sync', t => {
    t.test('.', t => runTest(t, '.', WriteEntry.Sync))
    return t.test('./', t => runTest(t, './', WriteEntry.Sync))
  })

  t.end()
})

t.test('hard links from tar entries and no prefix', t => {
  const expect = [
    {
      type: 'Directory',
      path: './',
    },
    {
      type: 'File',
      size: 3,
      path: 'target',
    },
    'ddd\0',
    {
      path: 'y',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/',
    },
    {
      type: 'Directory',
      path: 'z/b/',
    },
    {
      path: 'z/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/b/c/',
    },
    {
      path: 'z/b/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      path: 'z/b/c/d',
      type: 'Link',
      linkpath: 'target',
    },
  ]

  const data = makeTar([
    {
      type: 'Directory',
      path: './',
    },
    {
      type: 'File',
      size: 3,
      path: 'target',
    },
    'ddd\0',
    {
      path: 'y',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/',
    },
    {
      type: 'Directory',
      path: 'z/b/',
    },
    {
      path: 'z/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      type: 'Directory',
      path: 'z/b/c/',
    },
    {
      path: 'z/b/d',
      type: 'Link',
      linkpath: 'target',
    },
    {
      path: 'z/b/c/d',
      type: 'Link',
      linkpath: 'target',
    },
  ])

  const check = (out, t) => {
    const data = Buffer.concat(out)
    expect.forEach((e, i) => {
      if (typeof e === 'string') {
        t.equal(data.slice(i * 512, i * 512 + e.length).toString(), e)
      } else if (e instanceof RegExp) {
        t.match(data.slice(i * 512, (i + 1) * 512).toString(), e)
      } else {
        t.match(new Header(data.slice(i * 512, (i + 1) * 512)), e)
      }
    })
  }

  const runTest = async (t, path) => {
    const linkCache = new Map()
    const statCache = new Map()
    const opt = {
      noDirRecurse: true,
      linkCache,
      statCache,
    }
    const out = []
    const parser = new Parser({
      onentry: readEntry => {
        const p = new WriteEntry.Tar(readEntry, opt)
        p.on('data', d => out.push(d))
      },
    })
    parser.on('end', () => check(out, t))
    parser.end(data)
  }

  t.test('.', t => runTest(t, '.'))
  t.test('./', t => runTest(t, './'))

  t.end()
})

t.test('myuid set by getuid() if available, otherwise 0', t => {
  const { getuid } = process
  process.getuid = null
  const noUid = new WriteEntry(__filename)
  t.equal(noUid.myuid, 0, 'set to zero if no getuid function')
  process.getuid = () => 123456789
  const hasUid = new WriteEntry(__filename)
  t.equal(hasUid.myuid, 123456789, 'set to process.getuid()')
  process.getuid = getuid
  t.end()
})
