import fs from 'fs';
import assert from 'assert';
import MagicString from 'magic-string';
import { parse } from 'acorn';
import { Store } from '../../store.js';

describe('store', () => {
	it('is written in ES5', () => {
		const source = fs.readFileSync('store.js', 'utf-8');

		const ast = parse(source, {
			sourceType: 'module'
		});

		const magicString = new MagicString(source);
		ast.body.forEach(node => {
			if (/^(Im|Ex)port/.test(node.type)) magicString.remove(node.start, node.end);
		});

		parse(magicString.toString(), {
			ecmaVersion: 5
		});
	});

	describe('get', () => {
		it('gets a specific key', () => {
			const store = new Store({
				foo: 'bar'
			});

			assert.equal(store.get('foo'), 'bar');
		});

		it('gets the entire state object', () => {
			const store = new Store({
				foo: 'bar'
			});

			assert.deepEqual(store.get(), { foo: 'bar' });
		});
	});

	describe('set', () => {
		it('sets state', () => {
			const store = new Store();

			store.set({
				foo: 'bar'
			});

			assert.equal(store.get('foo'), 'bar');
		});
	});

	describe('observe', () => {
		it('observes state', () => {
			let newFoo;
			let oldFoo;

			const store = new Store({
				foo: 'bar'
			});

			store.observe('foo', (n, o) => {
				newFoo = n;
				oldFoo = o;
			});

			assert.equal(newFoo, 'bar');
			assert.equal(oldFoo, undefined);

			store.set({
				foo: 'baz'
			});

			assert.equal(newFoo, 'baz');
			assert.equal(oldFoo, 'bar');
		});
	});

	describe('onchange', () => {
		it('fires a callback when state changes', () => {
			const store = new Store();

			let count = 0;
			let args;

			store.onchange((state, changed) => {
				count += 1;
				args = { state, changed };
			});

			store.set({ foo: 'bar' });

			assert.equal(count, 1);
			assert.deepEqual(args, {
				state: { foo: 'bar' },
				changed: { foo: true }
			});

			// this should be a noop
			store.set({ foo: 'bar' });
			assert.equal(count, 1);

			// this shouldn't
			store.set({ foo: 'baz' });

			assert.equal(count, 2);
			assert.deepEqual(args, {
				state: { foo: 'baz' },
				changed: { foo: true }
			});
		});
	});

	it('allows user to cancel state change callback', () => {
		const store = new Store();
		const handler = store.onchange(() => {});

		assert.doesNotThrow(() => {
			handler.cancel();
		}, TypeError, 'this._changeHandlers is undefined');
	});

	describe('computed', () => {
		it('computes a property based on data', () => {
			const store = new Store({
				foo: 1
			});

			store.compute('bar', ['foo'], foo => foo * 2);
			assert.equal(store.get('bar'), 2);

			const values = [];

			store.observe('bar', bar => {
				values.push(bar);
			});

			store.set({ foo: 2 });
			assert.deepEqual(values, [2, 4]);
		});

		it('computes a property based on another computed property', () => {
			const store = new Store({
				foo: 1
			});

			store.compute('bar', ['foo'], foo => foo * 2);
			store.compute('baz', ['bar'], bar => bar * 2);
			assert.equal(store.get('baz'), 4);

			const values = [];

			store.observe('baz', baz => {
				values.push(baz);
			});

			store.set({ foo: 2 });
			assert.deepEqual(values, [4, 8]);
		});

		it('prevents computed properties from being set', () => {
			const store = new Store({
				foo: 1
			});

			store.compute('bar', ['foo'], foo => foo * 2);

			assert.throws(() => {
				store.set({ bar: 'whatever' });
			}, /'bar' is a read-only property/);
		});

		it('allows multiple dependents to depend on the same computed property', () => {
			const store = new Store({
				a: 1
			});

			store.compute('b', ['a'], a => a * 2);
			store.compute('c', ['b'], b => b * 3);
			store.compute('d', ['b'], b => b * 4);

			assert.deepEqual(store.get(), { a: 1, b: 2, c: 6, d: 8 });

			// bit cheeky, testing a private property, but whatever
			assert.equal(store._sortedComputedProperties.length, 3);
		});

		it('prevents cyclical dependencies', () => {
			const store = new Store();

			assert.throws(() => {
				store.compute('a', ['b'], b => b + 1);
				store.compute('b', ['a'], a => a + 1);
			}, /Cyclical dependency detected/);
		});
	});

	describe('immutable', () => {
		it('observing state only changes on immutable updates', () => {
			let newFoo;
			let oldFoo;
			let callCount = 0;
			let value1 = {};
			let value2 = {};

			const store = new Store({
				foo: value1
			}, { immutable: true });

			store.observe('foo', (n, o) => {
				callCount++;
				newFoo = n;
				oldFoo = o;
			});

			assert.equal(callCount, 1);
			assert.equal(newFoo, value1);
			assert.equal(oldFoo, undefined);

			store.set({
				foo: value1
			});

			assert.equal(callCount, 1);
			assert.equal(newFoo, value1);
			assert.equal(oldFoo, undefined);

			store.set({
				foo: value2
			});

			assert.equal(callCount, 2);
			assert.equal(newFoo, value2);
			assert.equal(oldFoo, value1);
		});
	});
});