/*
 * Run this file to update test files used to test specific types of videos.
 *
 *     node refresh.js [type] [no-request]
 *
 * Provide a `type` to only update the one video.
 *
 * Give `no-request` if you want to run with mocked requests using nock.
 *
 * If there are ever issues with a specific type of video, we can use this script
 * to update that type, investigate what changed, and run tests.
 */

const videos = [
  {
    id: '5qap5aO4i9A',
    type: 'live-now',
    saveInfo: true,
  },
  {
    id: '21X5lGlDOfg',
    type: 'live-with-cc',
  },
  {
    id: 'VIBFo3Ti5vQ',
    type: 'live-future',
  },
  {
    id: 'nu5uzMXfuLc',
    type: 'live-past',
    basicInfo: true,
  },
  {
    id: 'SyKPsFRP_Oc',
    type: 'rental',
    basicInfo: true,
    saveInfo: true,
  },
  {
    id: '_HSylqgVYQI',
    type: 'regular',
    keep: ['video.flv'],
    saveInfo: true,
  },
  {
    id: 'LuZu9N53Vd0',
    type: 'age-restricted',
    basicInfo: true,
    saveInfo: true,
  },
  {
    id: 'CsrfovOPcjk',
    type: 'age-restricted-vi-VN',
    lang: 'vi-VN',
    basicInfo: true,
    saveInfo: true,
  },
  {
    id: 'aqz-KE-bpKQ',
    type: 'use-backups',
    saveInfo: true,
    mock: {
      'watch.html': '<html></html>',
      'watch.json': '[]',
    },
  },
  {
    id: '99999999999',
    type: 'non-existent',
  },
  {
    id: 'XDNFAujgJb0',
    type: 'music',
    basicInfo: true,
    saveInfo: true,
    skip: ['watch.json', 'get_video_info'],
  },
  {
    id: 'xRu7qKijBso',
    type: 'game',
    basicInfo: true,
    saveInfo: true,
    skip: ['watch.json', 'get_video_info'],
  },
  {
    id: 'YQHsXMglC9A',
    type: 'cipher',
  },
  {
    id: 'GFg8BP01F5Q',
    type: 'no-embed',
    basicInfo: true,
  },
  {
    id: 'KKzOh0MRuZE',
    type: 'no-likes-or-dislikes',
    skip: ['get_video_info', /player/],
  },
  {
    id: '_HSylqgVYQI',
    type: 'rich-thumbnails',
    basicInfo: true,
    saveInfo: true,
    skip: ['watch.html', /player/],
  },
  {
    id: 'z2jeHsa0UG0',
    type: 'private',
  },
  {
    id: '_HSylqgVYQI',
    type: 'related-topics',
    needCookie: true,
    basicInfo: true,
    saveInfo: true,
  },
  {
    id: 'TceijYjxdrQ',
    type: 'longest-upload',
    basicInfo: true,
  },
  {
    id: 'W6NZfCO5SIk',
    type: 'chapters',
    basicInfo: true,
    saveInfo: true,
  },
];


const fs = require('fs');
const path = require('path');
const { PassThrough } = require('stream');
const mukRequire = require('muk-require');
const miniget = require('miniget');


// Don't check for update, otherwise, it'll get saved as part of a video request.
process.env.YTDL_NO_UPDATE = 'true';


// Tries to remove instances of your ip from saved test files.
const cleanBody = body => body
  // eslint-disable-next-line max-len
  .replace(/(ip(?:=|\\?\/|%3D|%253D|%2F))((?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|[0-9a-f]{1,4}(?:(?::|%3A)[0-9a-f]{1,4}){7})/ig, '$10.0.0.0');

// Returns true if `filename` is found in `video.skip`.
// `video.skip` can be a regex.
const skipFile = (video, filename) => video.skip && video.skip.some(skip =>
  skip instanceof RegExp ? skip.test(filename) : filename.includes(skip),
);

const playerfile = /((?:html5)?player[-_][a-zA-Z0-9\-_.]+)(?:\.js|\/)/;

const refreshVideo = async(video, noRequests) => {
  console.log('refreshing video:', video.id, video.type);
  const folder = path.join(__dirname, `videos/${video.type}`);
  let existingFiles = {};
  try {
    fs.accessSync(folder);
    let files = fs.readdirSync(folder);
    for (let filename of files) {
      existingFiles[filename] = false;
    }
    if (video.keep) {
      for (let filename of video.keep || []) {
        existingFiles[filename] = true;
      }
    }
  } catch (err) {
    console.log('creating folder:', folder);
    fs.mkdirSync(folder);
  }

  const writeFile = (filename, body) => {
    if (filename in existingFiles) {
      let oldBody = fs.readFileSync(path.join(folder, filename), 'utf8');
      if (oldBody === body) {
        existingFiles[filename] = true;
        return;
      }
      console.log('update file:', filename);
    } else {
      console.log('save file:', filename);
    }
    fs.writeFileSync(path.join(folder, filename), body);
    existingFiles[filename] = true;
  };

  const getFilenameFromURL = url => {
    let parsed = new URL(url);
    let s = parsed.pathname.split('/');
    let filename =
      // Special case for livestream manifest files.
      /\/manifest\/dash\//.test(url) ? 'dash-manifest.xml' :
        /\/manifest\/hls_(variant|playlist)\//.test(url) ? 'hls-manifest.m3u8' :

        // Save the key of html5player file so we know if they've changed.
          playerfile.test(url) ? `${playerfile.exec(url)[1]}.js` :

            // Save watch and embed pages with .html extension.
            /^\/watch$/.test(parsed.pathname) ?
              /pbj=1/.test(parsed.search) ? 'watch.json' : 'watch.html' :
              /^\/embed\//.test(parsed.pathname) ? 'embed.html' :

                // Otherwise, use url path as filename.
                s[s.length - 1];
    return filename;
  };

  const minigetMock = (url, options) => {
    let filename = getFilenameFromURL(url);
    console.log('request:', url.length > 100 ? `${url.slice(0, 97)}...` : url);

    // Save contents to file.
    const saveContents = body => {
      if ((!video.keep || video.keep.indexOf(filename) === -1) &&
          !skipFile(video, filename)) {
        body = cleanBody(body);
        writeFile(filename, body);
      }
    };

    let stream = miniget(url, options);
    let body = [];
    let resolveEnd;
    let streamEnd = new Promise(resolve => resolveEnd = resolve);
    stream.on('data', chunk => { body.push(chunk); });
    stream.on('end', () => {
      resolveEnd(true);
      saveContents(body.join(''));
    });
    if (video.mock && video.mock[filename] !== undefined) {
      const text = video.mock[filename];
      stream.resume();
      stream = new PassThrough();
      stream.text = () => streamEnd.then(() => text);
      setImmediate(() => {
        streamEnd.then(() => stream.end(text));
      });
    }
    return stream;
  };
  Object.assign(minigetMock, miniget);

  const getInfo = mukRequire('../../lib/info', { miniget: minigetMock });

  if (noRequests) {
    const nock = require('../nock');
    nock(video.id, video.type);
  }

  // Make the call to ytdl.
  try {
    let info;
    let options = { requestOptions: { maxRetries: 0 }, lang: 'en' };
    // Not implemented yet
    if (video.needCookie) return console.log('Need cookies to refresh this.');
    if (video.lang) options.lang = video.lang;
    if (video.basicInfo) {
      info = await getInfo.getBasicInfo(video.id, options);
    } else {
      info = await getInfo.getInfo(video.id, options);
    }
    if (video.saveInfo) {
      let filename = 'expected-info.json';
      let body = cleanBody(JSON.stringify(info, null, 2));
      writeFile(filename, body);
    }
  } catch (err) {
    console.log('error retrieving video info:', err.message);
    console.log(err.stack);
  }

  // Delete any files no longer used from the `getInfo()` call.
  for (let file in existingFiles) {
    if (!existingFiles[file]) {
      console.log('removing file:', file);
      fs.unlinkSync(path.join(folder, file));
    }
  }

  console.log();
};

const refreshAll = async noRequests => {
  for (let video of videos) {
    // eslint-disable-next-line no-await-in-loop
    await refreshVideo(video, noRequests);
  }
};


// Accept only refreshing one video type.
let type = process.argv[2];
let noRequests = !!process.argv[3];
if (type) {
  let video = videos.find(video2 => video2.type === type);
  if (video) {
    refreshVideo(video, noRequests);
  } else {
    console.error('video type not found', type);
  }
} else {
  refreshAll(noRequests);
}
