/* eslint-disable max-lines */

import { splitTextStyleAndMetadata } from '../shared/helpers.js';

describe('Tests for api', ()=>{
	let api;
	let google;
	let model;
	let hbBrew;
	let googleBrew;
	let res;

	let modelBrew;
	let saveFunc;
	let markModifiedFunc;
	let saved;

	beforeEach(()=>{
		jest.resetModules();
		jest.restoreAllMocks();

		saved = undefined;
		saveFunc = jest.fn(async function() {
			saved = { ...this, _id: '1' };
			return saved;
		});
		markModifiedFunc = jest.fn(()=>true);

		modelBrew = (brew)=>({
			...brew,
			save         : saveFunc,
			markModified : markModifiedFunc,
			toObject     : function() {
				delete this.save;
				delete this.toObject;
				delete this.markModified;
				return this;
			}
		});

		google = require('./googleActions.js').default;
		model  = require('./homebrew.model.js').model;
		api    = require('./homebrew.api').default;

		jest.mock('./googleActions.js');
		google.authCheck = jest.fn(()=>'client');
		google.newGoogleBrew = jest.fn(()=>'id');
		google.deleteGoogleBrew = jest.fn(()=>true);

		jest.mock('./homebrew.model.js');
		model.mockImplementation((brew)=>modelBrew(brew));

		res = {
			status    : jest.fn(()=>res),
			send      : jest.fn(()=>{}),
			set       : jest.fn(()=>{}),
			setHeader : jest.fn(()=>{})
		};

		hbBrew = {
			text        : `brew text`,
			style       : 'hello yes i am css',
			title       : 'some title',
			description : 'this is a description',
			tags        : ['something', 'fun'],
			lang        : 'en',
			renderer    : 'v3',
			theme       : 'phb',
			published   : true,
			authors     : ['1', '2'],
			owner       : '1',
			thumbnail   : '',
			_id         : 'mongoid',
			editId      : 'abcdefg',
			shareId     : 'hijklmnop',
			views       : 1,
			lastViewed  : new Date(),
			version     : 1,
			pageCount   : 1,
			textBin     : '',
			views       : 0
		};
		googleBrew = {
			...hbBrew,
			googleId : '12345'
		};
	});

	describe('getId', ()=>{
		it('should return only id if google id is not present', ()=>{
			const { id, googleId } = api.getId({
				params : {
					id : 'abcdefgh'
				}
			});

			expect(id).toEqual('abcdefgh');
			expect(googleId).toBeUndefined();
		});

		it('should throw if id is too short', ()=>{
			let err;
			try {
				api.getId({
					params : {
						id : 'abcd'
					}
				});
			} catch (e) {
				err = e;
			};

			expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 });
		});

		it('should return id and google id from request body', ()=>{
			const { id, googleId } = api.getId({
				params : {
					id : 'abcdefghijkl'
				},
				body : {
					googleId : '123456789012345678901234567890123'
				}
			});

			expect(id).toEqual('abcdefghijkl');
			expect(googleId).toEqual('123456789012345678901234567890123');
		});

		it('should throw invalid - google id right length but does not match pattern', ()=>{
			let err;
			try {
				api.getId({
					params : {
						id : 'abcdefghijkl'
					},
					body : {
						googleId : '012345678901234567890123456789012'
					}
				});
			} catch (e) {
				err = e;
			}

			expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
		});

		it('should throw invalid - google id too short (32 char)', ()=>{
			let err;
			try {
				api.getId({
					params : {
						id : 'abcdefghijkl'
					},
					body : {
						googleId : '12345678901234567890123456789012'
					}
				});
			} catch (e) {
				err = e;
			}

			expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
		});

		it('should throw invalid - google id too long (45 char)', ()=>{
			let err;
			try {
				api.getId({
					params : {
						id : 'abcdefghijkl'
					},
					body : {
						googleId : '123456789012345678901234567890123456789012345'
					}
				});
			} catch (e) {
				err = e;
			}

			expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
		});

		it('should return 12-char id and google id from params', ()=>{
			const { id, googleId } = api.getId({
				params : {
					id : '123456789012345678901234567890123abcdefghijkl'
				}
			});

			expect(googleId).toEqual('123456789012345678901234567890123');
			expect(id).toEqual('abcdefghijkl');
		});

		it('should return 10-char id and google id from params', ()=>{
			const { id, googleId } = api.getId({
				params : {
					id : '123456789012345678901234567890123abcdefghij'
				}
			});

			expect(googleId).toEqual('123456789012345678901234567890123');
			expect(id).toEqual('abcdefghij');
		});
	});
	describe('getBrew', ()=>{
		const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
		const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };

		it('returns middleware', ()=>{
			const getFn = api.getBrew('share');
			expect(getFn).toBeInstanceOf(Function);
		});

		it('should fetch from mongoose', async ()=>{
			const testBrew = { title: 'test brew', authors: [] };
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise(testBrew));

			const fn = api.getBrew('share', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);

			expect(req.brew).toEqual(testBrew);
			expect(next).toHaveBeenCalled();
			expect(api.getId).toHaveBeenCalledWith(req);
			expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
		});

		it('should handle mongoose error', async ()=>{
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew')));

			const fn = api.getBrew('share', false);
			const req = { brew: {} };
			const next = jest.fn();
			let err;
			try {
				await fn(req, null, next);
			} catch (e) {
				err = e;
			}

			expect(err).toEqual(notFoundError);
			expect(req.brew).toEqual({});
			expect(next).not.toHaveBeenCalled();
			expect(api.getId).toHaveBeenCalledWith(req);
			expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
		});

		it('changes tags from string to array', async ()=>{
			const testBrew = { title: 'test brew', authors: [], tags: '' };
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise(testBrew));

			const fn = api.getBrew('share', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);

			expect(req.brew.tags).toEqual([]);
			expect(next).toHaveBeenCalled();
		});

		it('throws if not logged in as author', async ()=>{
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));

			const fn = api.getBrew('edit', true);
			const req = { brew: {} };

			let err;
			try {
				await fn(req, null, null);
			} catch (e) {
				err = e;
			}

			expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
		});

		it('throws if logged in as invalid author', async ()=>{
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));

			const fn = api.getBrew('edit', true);
			const req = { brew: {}, account: { username: 'b' } };

			let err;
			try {
				await fn(req, null, null);
			} catch (e) {
				err = e;
			}

			expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
		});

		it('does not throw if no authors', async ()=>{
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] }));

			const fn = api.getBrew('edit', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);

			expect(next).toHaveBeenCalled();
			expect(req.brew.title).toEqual('test brew');
			expect(req.brew.authors).toEqual([]);
		});

		it('does not throw if valid author', async ()=>{
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));

			const fn = api.getBrew('edit', true);
			const req = { brew: {}, account: { username: 'a' } };
			const next = jest.fn();
			await fn(req, null, next);

			expect(next).toHaveBeenCalled();
			expect(req.brew.title).toEqual('test brew');
			expect(req.brew.authors).toEqual(['a']);
		});

		it('fetches google brew if needed', async()=>{
			const stubBrew = { title: 'test brew', authors: ['a'] };
			const googleBrew = { title: 'test google brew', text: 'brew text' };
			api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
			model.get = jest.fn(()=>toBrewPromise(stubBrew));
			google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew)));

			const fn = api.getBrew('share', false);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);

			expect(req.brew).toEqual({
				title       : 'test google brew',
				authors     : ['a'],
				text        : 'brew text',
				stubbed     : true,
				description : '',
				editId      : undefined,
				pageCount   : 1,
				published   : false,
				renderer    : 'legacy',
				lang        : 'en',
				shareId     : undefined,
				tags        : [],
				theme       : '5ePHB',
				thumbnail   : '',
				textBin     : undefined,
				version     : undefined,
				createdAt   : undefined,
				gDrive      : false,
				style       : undefined,
				trashed     : false,
				updatedAt   : undefined,
				views       : 0
			});
			expect(next).toHaveBeenCalled();
			expect(api.getId).toHaveBeenCalledWith(req);
			expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
			expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share');
		});

		it('access is denied to a locked brew', async()=>{
			const lockBrew = { title: 'test brew', shareId: '1', lock: { code: 404, shareMessage: 'brew locked' } };
			model.get = jest.fn(()=>toBrewPromise(lockBrew));
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));

			const fn = api.getBrew('share', false);
			const req = { brew: {} };
			const next = jest.fn();

			await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
		});
	});
	describe('Get CSS', ()=>{
		it('should return brew style content as CSS text', async ()=>{
			const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise(testBrew));

			const fn = api.getBrew('share', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);
			await api.getCSS(req, res);

			expect(req.brew).toEqual(testBrew);
			expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
			expect(res.set).toHaveBeenCalledWith({
				'Cache-Control' : 'no-cache',
				'Content-Type'  : 'text/css'
			});
		});

		it('should return 404 when brew has no style content', async ()=>{
			const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise(testBrew));

			const fn = api.getBrew('share', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);
			await api.getCSS(req, res);

			expect(req.brew).toEqual(testBrew);
			expect(req.brew).toHaveProperty('style');
			expect(res.status).toHaveBeenCalledWith(404);
			expect(res.send).toHaveBeenCalledWith('');
		});

		it('should return 404 when brew does not exist', async ()=>{
			const testBrew = { };

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
			model.get = jest.fn(()=>toBrewPromise(testBrew));

			const fn = api.getBrew('share', true);
			const req = { brew: {} };
			const next = jest.fn();
			await fn(req, null, next);
			await api.getCSS(req, res);

			expect(req.brew).toEqual(testBrew);
			expect(req.brew).toHaveProperty('style');
			expect(res.status).toHaveBeenCalledWith(404);
			expect(res.send).toHaveBeenCalledWith('');
		});
	});
	describe('mergeBrewText', ()=>{
		it('should set metadata and no style if it is not present', ()=>{
			const result = api.mergeBrewText({
				text        : `brew`,
				title       : 'some title',
				description : 'this is a description',
				tags        : ['something', 'fun'],
				renderer    : 'v3',
				theme       : 'phb',
				googleId    : '12345'
			});

			expect(result).toEqual(`\`\`\`metadata
title: some title
description: this is a description
tags:
  - something
  - fun
renderer: v3
theme: phb

\`\`\`

brew`);
		});

		it('should set metadata and style', ()=>{
			const result = api.mergeBrewText({
				text        : `brew`,
				style       : 'hello yes i am css',
				title       : 'some title',
				description : 'this is a description',
				tags        : ['something', 'fun'],
				renderer    : 'v3',
				theme       : 'phb',
				googleId    : '12345'
			});

			expect(result).toEqual(`\`\`\`metadata
title: some title
description: this is a description
tags:
  - something
  - fun
renderer: v3
theme: phb

\`\`\`

\`\`\`css
hello yes i am css
\`\`\`

brew`);
		});
	});
	describe('exclusion methods', ()=>{
		it('excludePropsFromUpdate removes the correct keys', ()=>{
			const sent = Object.assign({}, googleBrew);
			const result = api.excludePropsFromUpdate(sent);

			expect(sent).toEqual(googleBrew);
			expect(result._id).toBeUndefined();
			expect(result.views).toBeUndefined();
			expect(result.lastViewed).toBeUndefined();
		});

		it('excludeGoogleProps removes the correct keys', ()=>{
			const sent = Object.assign({}, googleBrew);
			const result = api.excludeGoogleProps(sent);

			expect(sent).toEqual(googleBrew);
			expect(result.tags).toBeUndefined();
			expect(result.published).toBeUndefined();
			expect(result.authors).toBeUndefined();
			expect(result.owner).toBeUndefined();
			expect(result.views).toBeUndefined();
			expect(result.thumbnail).toBeUndefined();
			expect(result.version).toBeUndefined();
		});

		it('excludeStubProps removes the correct keys from the original object', ()=>{
			const sent = Object.assign({}, googleBrew);
			const result = api.excludeStubProps(sent);

			expect(sent).not.toEqual(googleBrew);
			expect(result.text).toBeUndefined();
			expect(result.textBin).toBeUndefined();
			expect(result.renderer).toBe('v3');
			expect(result.pageCount).toBe(1);
		});
	});
	describe('beforeNewSave', ()=>{
		it('sets the title if none', ()=>{
			const brew = {
				...hbBrew,
				title : undefined
			};
			api.beforeNewSave({}, brew);

			expect(brew.title).toEqual('brew text');
		});

		it('does not override the title if present', ()=>{
			const brew = {
				...hbBrew,
				title : 'test'
			};
			api.beforeNewSave({}, brew);

			expect(brew.title).toEqual('test');
		});

		it('does not set authors if account missing username', ()=>{
			api.beforeNewSave({}, hbBrew);

			expect(hbBrew.authors).toEqual([]);
		});

		it('sets authors if account has username', ()=>{
			api.beforeNewSave({ username: 'hi' }, hbBrew);

			expect(hbBrew.authors).toEqual(['hi']);
		});

		it('merges brew text', ()=>{
			api.mergeBrewText = jest.fn(()=>'merged');
			api.beforeNewSave({}, hbBrew);

			expect(api.mergeBrewText).toHaveBeenCalled();
			expect(hbBrew.text).toEqual('merged');
		});
	});
	describe('newGoogleBrew', ()=>{
		it('should call the correct methods', ()=>{
			api.excludeGoogleProps = jest.fn(()=>'newBrew');

			const acct = { username: 'test' };
			const brew = { title: 'test title' };
			api.newGoogleBrew(acct, brew, res);

			expect(google.authCheck).toHaveBeenCalledWith(acct, res);
			expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew);
			expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
		});
	});
	describe('newBrew', ()=>{
		it('should set up a default brew via Homebrew model', async ()=>{
			await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				_id         : '1',
				authors     : ['test user'],
				createdAt   : undefined,
				description : '',
				editId      : expect.any(String),
				gDrive      : false,
				pageCount   : 1,
				published   : false,
				renderer    : 'V3',
				lang        : 'en',
				shareId     : expect.any(String),
				style       : undefined,
				tags        : [],
				text        : undefined,
				textBin     : expect.objectContaining({}),
				theme       : '5ePHB',
				thumbnail   : '',
				title       : 'asdf',
				trashed     : false,
				updatedAt   : undefined,
				views       : 0
			});
		});

		it('should remove edit/share/google ids', async ()=>{
			await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalled();
			const sent = res.send.mock.calls[0][0];
			expect(sent.editId).not.toEqual('1234');
			expect(sent.shareId).not.toEqual('1234');
			expect(sent.googleId).toBeUndefined();
		});

		it('should handle mongo error', async ()=>{
			saveFunc = jest.fn(async function() {
				throw 'err';
			});
			model.mockImplementation((brew)=>modelBrew(brew));

			let err;
			try {
				await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
			} catch (e) {
				err = e;
			}

			expect(res.send).not.toHaveBeenCalled();
			expect(err).not.toBeUndefined();
		});

		it('should save to google if requested', async()=>{
			await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);

			expect(google.newGoogleBrew).toHaveBeenCalled();
			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				_id         : '1',
				authors     : ['test user'],
				createdAt   : undefined,
				description : '',
				editId      : expect.any(String),
				gDrive      : false,
				pageCount   : 1,
				published   : false,
				renderer    : 'V3',
				lang        : 'en',
				shareId     : expect.any(String),
				googleId    : expect.any(String),
				style       : undefined,
				tags        : [],
				text        : undefined,
				textBin     : undefined,
				theme       : '5ePHB',
				thumbnail   : '',
				title       : 'asdf',
				trashed     : false,
				updatedAt   : undefined,
				views       : 0
			});
		});
	});
	describe('Theme bundle', ()=>{
		it('should return Theme Bundle for a User Theme', async ()=>{
			const brews = {
				userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] }
			};

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
			const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			await api.getThemeBundle(req, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				name     : 'User Theme A',
				author   : 'authorName',
				styles   : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
				snippets : []
			});
		});

		it('should return Theme Bundle for nested User Themes', async ()=>{
			const brews = {
				userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
				userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
				userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
			};

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
			const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			await api.getThemeBundle(req, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				name   : 'User Theme A',
				author : 'authorName',
				styles : [
					'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
					'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
					'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
				],
				snippets : []
			});
		});

		it('should return Theme Bundle for a Static Theme', async ()=>{
			const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			await api.getThemeBundle(req, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				name   : '5ePHB',
				author : undefined,
				styles : [
					`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
					`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
				],
				snippets : [
					'V3_Blank',
					'V3_5ePHB'
				]
			});
		});

		it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
			const brews = {
				userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
				userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
				userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
			};

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
			const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			await api.getThemeBundle(req, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith({
				name   : 'User Theme A',
				author : 'authorName',
				styles : [
					`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
					`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
					`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
					'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
					'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
					'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
				],
				snippets : [
					'V3_Blank',
					'V3_5ePHB',
					'V3_5eDMG'
				]
			});
		});

		it('should fail for a missing Theme in the chain', async()=>{
			const brews = {
				userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
			};

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
			const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			let err;
			await api.getThemeBundle(req, res)
			.catch((e)=>err = e);

			expect(err).toEqual({
				HBErrorCode : '09',
				accessType  : 'share',
				brewId      : 'missingTheme',
				message     : 'Theme Not Found',
				name        : 'ThemeLoad Error',
				status      : 404 });
		});

		it('should fail for a User Theme not tagged with meta:theme', async ()=>{
			const brews = {
				userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
			};

			const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
			model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
			const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };

			let err;
			await api.getThemeBundle(req, res)
			.catch((e)=>err = e);

			expect(err).toEqual({
				HBErrorCode : '10',
				brewId      : 'userThemeAID',
				message     : 'Selected theme does not have the meta:theme tag',
				name        : 'Invalid Theme Selected',
				status      : 422 });
		});
	});
	describe('updateBrew', ()=>{
		it('should return error on version mismatch', async ()=>{
			const brewFromClient = { version: 1 };
			const brewFromServer = { version: 1000, text: '' };

			const req = {
				brew : brewFromServer,
				body : brewFromClient
			};

			await api.updateBrew(req, res);

			expect(res.status).toHaveBeenCalledWith(409);
			expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
		});

		it('should return error on hash mismatch', async ()=>{
			const brewFromClient = { version: 1, hash: '1234' };
			const brewFromServer = { version: 1, text: 'test' };

			const req = {
				brew : brewFromServer,
				body : brewFromClient
			};

			await api.updateBrew(req, res);

			expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
			expect(res.status).toHaveBeenCalledWith(409);
			expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
		});

		// Commenting this one out for now, since we are no longer throwing this error while we monitor
		// it('should return error on applying patches', async ()=>{
		// 	const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
		// 	const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };

		// 	const req = {
		// 		brew  : brewFromServer,
		// 		body  : brewFromClient,
		// 	};

		// 	let err;
		// 	try {
		// 		await api.updateBrew(req, res);
		// 	} catch (e) {
		// 		err = e;
		// 	}

		// 	expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
		// });

		it('should save brew, no ID', async ()=>{
			const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
			const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };

			model.save = jest.fn((brew)=>{return brew;});

			const req = {
				brew  : brewFromServer,
				body  : brewFromClient,
				query : { saveToGoogle: false, removeFromGoogle: false }
			};

			await api.updateBrew(req, res);

			expect(res.status).toHaveBeenCalledWith(200);
			expect(res.send).toHaveBeenCalledWith(
				expect.objectContaining({
					_id         : '1',
					description : 'Test Description',
					hash        : '098f6bcd4621d373cade4e832627b4f6',
					title       : 'Test Title',
					version     : 2
				})
			);
		});
	});
	
	describe('deleteGoogleBrew', ()=>{
		it('should check auth and delete brew', async ()=>{
			const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);

			expect(result).toBe(true);
			expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
			expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
		});
	});
	describe('deleteBrew', ()=>{
		it('should handle case where fetching the brew returns an error', async ()=>{
			api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
			api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
			model.deleteOne = jest.fn(async ()=>{});
			const next = jest.fn(()=>{});

			await api.deleteBrew(null, null, next);

			expect(next).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalledWith({ editId: '1' });
		});

		it('should delete if no authors', async ()=>{
			const brew = {
				...hbBrew,
				authors : []
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			const req = {};

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalled();
		});

		it('should throw on delete error', async ()=>{
			const brew = {
				...hbBrew,
				authors : []
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{ throw 'err'; });
			const req = {};

			let err;
			try {
				await api.deleteBrew(req, res);
			} catch (e) {
				err = e;
			}

			expect(err).not.toBeUndefined();
			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalled();
		});

		it('should delete when one author', async ()=>{
			const brew = {
				...hbBrew,
				authors : ['test']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			const req = { account: { username: 'test' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalled();
		});

		it('should remove one author when multiple present', async ()=>{
			const brew = {
				...hbBrew,
				authors : ['test', 'test2']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			const req = { account: { username: 'test' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(markModifiedFunc).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).not.toHaveBeenCalled();
			expect(saveFunc).toHaveBeenCalled();
			expect(saved.authors).toEqual(['test2']);
		});

		it('should handle save error', async ()=>{
			const brew = {
				...hbBrew,
				authors : ['test', 'test2']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			saveFunc = jest.fn(async ()=>{ throw 'err'; });
			const req = { account: { username: 'test' } };

			let err;
			try {
				await api.deleteBrew(req, res);
			} catch (e) {
				err = e;
			}

			expect(err).not.toBeUndefined();
			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).not.toHaveBeenCalled();
			expect(saveFunc).toHaveBeenCalled();
		});

		it('should delete google brew', async ()=>{
			const brew = {
				...googleBrew,
				authors : ['test']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			api.deleteGoogleBrew = jest.fn(async ()=>true);
			const req = { account: { username: 'test' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalled();
			expect(api.deleteGoogleBrew).toHaveBeenCalled();
		});

		it('should handle google brew delete error', async ()=>{
			const brew = {
				...googleBrew,
				authors : ['test']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			api.deleteGoogleBrew = jest.fn(async ()=>{
				 throw 'err';
			});
			const req = { account: { username: 'test' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).toHaveBeenCalled();
			expect(api.deleteGoogleBrew).toHaveBeenCalled();
		});

		it('should delete google brew and retain stub when multiple authors and owner request deletion', async ()=>{
			const brew = {
				...googleBrew,
				authors : ['test', 'test2']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			api.deleteGoogleBrew = jest.fn(async ()=>true);
			const req = { account: { username: 'test' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(markModifiedFunc).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).not.toHaveBeenCalled();
			expect(api.deleteGoogleBrew).toHaveBeenCalled();
			expect(saveFunc).toHaveBeenCalled();
			expect(saved.authors).toEqual(['test2']);
			expect(saved.googleId).toEqual(undefined);
			expect(saved.text).toEqual(undefined);
			expect(saved.textBin).not.toEqual(undefined);
		});

		it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
			const brew = {
				...googleBrew,
				authors : ['test', 'test2']
			};
			api.getBrew = jest.fn(()=>async (req)=>{
				req.brew = brew;
			});
			model.findOne = jest.fn(async ()=>modelBrew(brew));
			model.deleteOne = jest.fn(async ()=>{});
			api.deleteGoogleBrew = jest.fn(async ()=>true);
			const req = { account: { username: 'test2' } };

			await api.deleteBrew(req, res);

			expect(api.getBrew).toHaveBeenCalled();
			expect(model.findOne).toHaveBeenCalled();
			expect(model.deleteOne).not.toHaveBeenCalled();
			expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
			expect(saveFunc).toHaveBeenCalled();
			expect(saved.authors).toEqual(['test']);
			expect(saved.googleId).toEqual(brew.googleId);
		});
	});
	
	describe('Split Text, Style, and Metadata', ()=>{

		it('basic splitting', async ()=>{
			const testBrew = {
				text : '```metadata\n' +
					'title: title\n' +
					'description: description\n' +
					'tags: [ \'tag a\' , \'tag b\' ]\n' +
					'renderer: legacy\n' +
					'theme: 5ePHB\n' +
					'lang: en\n' +
					'\n' +
					'```\n' +
					'\n' +
					'```css\n' +
					'style\n' +
					'style\n' +
					'style\n' +
					'```\n' +
					'\n' +
					'text\n'
			};

			splitTextStyleAndMetadata(testBrew);

			// Metadata
			expect(testBrew.title).toEqual('title');
			expect(testBrew.description).toEqual('description');
			expect(testBrew.renderer).toEqual('legacy');
			expect(testBrew.theme).toEqual('5ePHB');
			expect(testBrew.lang).toEqual('en');
			// Style
			expect(testBrew.style).toEqual('style\nstyle\nstyle\n');
			// Text
			expect(testBrew.text).toEqual('text\n');
		});
	});
	
});
