// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import expect = require('expect.js');

import {
  IChangedArgs
} from '../../../../lib/common/interfaces';

import {
  nbformat
} from '../../../../lib/notebook/notebook/nbformat';

import {
  CellModel, RawCellModel, MarkdownCellModel, CodeCellModel
} from '../../../../lib/notebook/cells';

import {
  OutputAreaModel
} from '../../../../lib/notebook/output-area';


class TestModel extends CellModel {
  get type(): 'raw' { return 'raw'; }

  setCursorData(name: string, newValue: any): void {
    super.setCursorData(name, newValue);
  }
}


describe('notebook/cells/model', () => {

  describe('CellModel', () => {

    describe('#constructor()', () => {

      it('should create a cell model', () => {
        let model = new CellModel();
        expect(model).to.be.a(CellModel);
      });

      it('should accept a base cell argument', () => {
        let base: nbformat.IBaseCell = {
          cell_type: 'raw',
          source: 'foo',
          metadata: { trusted: false }
        };
        let model = new CellModel(base);
        expect(model).to.be.a(CellModel);
        expect(model.source).to.equal(base.source);
      });

      it('should accept a base cell argument with a multiline source', () => {
        let base: nbformat.IBaseCell = {
          cell_type: 'raw',
          source: ['foo', 'bar', 'baz'],
          metadata: { trusted: false }
        };
        let model = new CellModel(base);
        expect(model).to.be.a(CellModel);
        expect(model.source).to.equal((base.source as string[]).join('\n'));
      });

    });

    describe('#contentChanged', () => {

      it('should signal when model content has changed', () => {
        let model = new CellModel();
        let called = false;
        model.contentChanged.connect(() => { called = true; });
        expect(called).to.be(false);
        model.source = 'foo';
        expect(called).to.be(true);
      });

    });

    describe('#stateChanged', () => {

      it('should signal when model state has changed', () => {
        let model = new CellModel();
        let listener = (sender: any, args: IChangedArgs<any>) => {
          value = args.newValue;
        };
        let value = '';
        model.stateChanged.connect(listener);
        expect(value).to.be.empty();
        model.source = 'foo';
        expect(value).to.be(model.source);
      });

      it('should not signal when model state has not changed', () => {
        let model = new CellModel();
        let called = 0;
        model.stateChanged.connect(() => { called++; });
        expect(called).to.be(0);
        model.source = 'foo';
        expect(called).to.be(1);
        model.source = 'foo';
        expect(called).to.be(1);
      });

    });

    describe('#metadataChanged', () => {

      it('should signal when model metadata has changed', () => {
        let model = new TestModel();
        let listener = (sender: any, args: IChangedArgs<any>) => {
          value = args.newValue;
        };
        let value = '';
        model.metadataChanged.connect(listener);
        expect(value).to.be.empty();
        model.setCursorData('foo', 'bar');
        expect(value).to.be('bar');
      });

      it('should not signal when model metadata has not changed', () => {
        let model = new TestModel();
        let called = 0;
        model.metadataChanged.connect(() => { called++; });
        expect(called).to.be(0);
        model.setCursorData('foo', 'bar');
        expect(called).to.be(1);
        model.setCursorData('foo', 'bar');
        expect(called).to.be(1);
      });

    });

    describe('#source', () => {

      it('should default to an empty string', () => {
        let model = new CellModel();
        expect(model.source).to.be.empty();
      });

      it('should be settable', () => {
        let model = new CellModel();
        expect(model.source).to.be.empty();
        model.source = 'foo';
        expect(model.source).to.be('foo');
      });

    });

    describe('#isDisposed', () => {

      it('should be false by default', () => {
        let model = new CellModel();
        expect(model.isDisposed).to.be(false);
      });

      it('should be true after model is disposed', () => {
        let model = new CellModel();
        model.dispose();
        expect(model.isDisposed).to.be(true);
      });

    });

    describe('#dispose()', () => {

      it('should dispose of the resources held by the model', () => {
        let model = new TestModel();

        model.setCursorData('foo', 'bar');
        expect(model.getMetadata('foo').getValue()).to.be('bar');
        model.dispose();
        expect(model.isDisposed).to.be(true);
        expect(model.getMetadata('foo')).to.be(null);
      });

      it('should be safe to call multiple times', () => {
        let model = new CellModel();
        model.dispose();
        model.dispose();
        expect(model.isDisposed).to.be(true);
      });

    });

    describe('#toJSON()', () => {

      it('should return a base cell encapsulation of the model value', () => {
        let base: nbformat.IBaseCell = {
          cell_type: 'raw',
          source: 'foo',
          metadata: { trusted: false }
        };
        let model = new TestModel(base);
        expect(model.toJSON()).to.not.equal(base);
        expect(model.toJSON()).to.eql(base);
      });

      it('should always return a string source', () => {
        let base: nbformat.IBaseCell = {
          cell_type: 'raw',
          source: ['foo', 'bar', 'baz'],
          metadata: { trusted: false }
        };
        let model = new TestModel(base);
        base.source = (base.source as string[]).join('\n');
        expect(model.toJSON()).to.not.equal(base);
        expect(model.toJSON()).to.eql(base);
      });

    });

    describe('#getMetadata()', () => {

      it('should get a metadata cursor for the cell', () => {
        let model = new CellModel();
        let c1 = model.getMetadata('foo');

        expect(c1.getValue()).to.be(void 0);
        c1.setValue(1);
        expect(c1.getValue()).to.be(1);

        let c2 = model.getMetadata('foo');

        expect(c2.getValue()).to.be(1);
        c2.setValue(2);
        expect(c1.getValue()).to.be(2);
        expect(c2.getValue()).to.be(2);
      });

    });

    describe('#listMetadata()', () => {

      it('should get a list of user metadata keys', () => {
        let model = new CellModel();
        let cursor = model.getMetadata('foo');
        expect(model.listMetadata()).to.be.empty();
        cursor.setValue(1);
        expect(model.listMetadata()).to.eql(['foo']);
      });

    });

  });

  describe('RawCellModel', () => {

    describe('#type', () => {

      it('should be set with type "raw"', () => {
        let model = new RawCellModel();
        expect(model.type).to.be('raw');
      });

    });

  });

  describe('MarkdownCellModel', () => {

    describe('#type', () => {

      it('should be set with type "markdown"', () => {
        let model = new MarkdownCellModel();
        expect(model.type).to.be('markdown');
      });

    });

  });

  describe('CodeCellModel', () => {

    describe('#constructor()', () => {

      it('should create a code cell model', () => {
        let model = new CodeCellModel();
        expect(model).to.be.a(CodeCellModel);
      });

      it('should accept a code cell argument', () => {
        let cell: nbformat.ICodeCell = {
          cell_type: 'code',
          execution_count: 1,
          outputs: [
            {
              output_type: 'display_data',
              data: { 'text/plain': 'foo' },
              metadata: {}
            } as nbformat.IDisplayData
          ],
          source: 'foo',
          metadata: { trusted: false }
        };
        let model = new CodeCellModel(cell);
        expect(model).to.be.a(CodeCellModel);
        expect(model.source).to.equal(cell.source);
      });

      it('should connect the outputs changes to content change signal', () => {
        let data = {
          output_type: 'display_data',
          data: { 'text/plain': 'foo' },
          metadata: {}
        } as nbformat.IDisplayData;
        let model = new CodeCellModel();
        let called = false;
        model.contentChanged.connect(() => { called = true; });
        expect(called).to.be(false);
        model.outputs.add(data);
        expect(called).to.be(true);
      });

    });

    describe('#type', () => {

      it('should be set with type "code"', () => {
        let model = new CodeCellModel();
        expect(model.type).to.be('code');
      });

    });

    describe('#executionCount', () => {

      it('should show the execution count of the cell', () => {
        let cell: nbformat.ICodeCell = {
          cell_type: 'code',
          execution_count: 1,
          outputs: [],
          source: 'foo',
          metadata: { trusted: false }
        };
        let model = new CodeCellModel(cell);
        expect(model.executionCount).to.be(1);
      });

      it('should be settable', () => {
        let model = new CodeCellModel();
        expect(model.executionCount).to.be(null);
        model.executionCount = 1;
        expect(model.executionCount).to.be(1);
      });

      it('should emit a state change signal when set', () => {
        let model = new CodeCellModel();
        let called = false;
        model.stateChanged.connect(() => { called = true; });
        expect(model.executionCount).to.be(null);
        expect(called).to.be(false);
        model.executionCount = 1;
        expect(model.executionCount).to.be(1);
        expect(called).to.be(true);
      });

      it('should not signal when state has not changed', () => {
        let model = new CodeCellModel();
        let called = 0;
        model.stateChanged.connect(() => { called++; });
        expect(model.executionCount).to.be(null);
        expect(called).to.be(0);
        model.executionCount = 1;
        expect(model.executionCount).to.be(1);
        model.executionCount = 1;
        expect(called).to.be(1);
      });

    });

    describe('#outputs', () => {

      it('should be an output area model', () => {
        let model = new CodeCellModel();
        expect(model.outputs).to.be.an(OutputAreaModel);
      });

    });

    describe('#dispose()', () => {

      it('should dispose of the resources held by the model', () => {
        let model = new CodeCellModel();
        expect(model.outputs).to.be.an(OutputAreaModel);
        model.dispose();
        expect(model.isDisposed).to.be(true);
        expect(model.outputs).to.be(null);
      });

      it('should be safe to call multiple times', () => {
        let model = new CodeCellModel();
        model.dispose();
        model.dispose();
        expect(model.isDisposed).to.be(true);
      });

    });

    describe('#toJSON()', () => {

      it('should return a code cell encapsulation of the model value', () => {
        let cell: nbformat.ICodeCell = {
          cell_type: 'code',
          execution_count: 1,
          outputs: [
            {
              output_type: 'display_data',
              data: { 'text/plain': 'foo' },
              metadata: {}
            } as nbformat.IDisplayData
          ],
          source: 'foo',
          metadata: { trusted: false }
        };
        let model = new CodeCellModel(cell);
        expect(model.toJSON()).to.not.equal(cell);
        expect(model.toJSON()).to.eql(cell);
      });

    });

  });

});
