import { shallow } from 'enzyme';
import { assoc, dissoc } from 'ramda';
import React from 'react';
import { Placeholder } from 'semantic-ui-react';

import { Collection, mapStateToProps } from './Collection';
import { Site } from '../../../tests/storyshare';

describe('mapStateToProps', () => {
  const site = {
    getResponseByRoute: jest.fn(),
  };

  beforeEach(() => {
    site.getResponseByRoute.mockClear();
  });

  it('retrieves the response from the DataLayer cache', () => {
    const props = { params: { }, site, typeSlug: 'type-slug' };
    mapStateToProps(null, props);

    expect(site.getResponseByRoute).toHaveBeenCalled();
    expect(site.getResponseByRoute)
      .toHaveBeenCalledWith(props.typeSlug, props.params);
  });

  it('passes the collection to the component', () => {
    site.getResponseByRoute.mockReturnValue({ body: 'test collection' });
    expect(mapStateToProps(null, { site }).collection).toBe('test collection');
  });

  it('passes the total count to the component', () => {
    site.getResponseByRoute.mockReturnValue({ total: 42 });
    expect(mapStateToProps(null, { site }).pagination.total).toBe(42);
  });

  it('passes the total page count to the component', () => {
    site.getResponseByRoute.mockReturnValue({ totalPages: 42 });
    expect(mapStateToProps(null, { site }).pagination.totalPages).toBe(42);
  });

  it('copes if the response is not available', () => {
    site.getResponseByRoute.mockReturnValue(undefined);
    const result = mapStateToProps(null, { site });
    expect(result).not.toHaveProperty('collection');
    expect(result).not.toHaveProperty('pagination');
  });
});

describe('Collection component', () => {
  let wrapper;

  const COLLECTION_FIXTURE = [{ id: 1 }, { id: 2 }];

  const onUpdate = jest.fn();

  const defaultProps = {
    collection: COLLECTION_FIXTURE,
    onUpdate,
    pagination: { total: 42, totalPages: 5 },
    params: { },
    site: new Site(),
    typeSlug: 'type-slug',
  };

  beforeEach(() => {
    wrapper = shallow(
      <Collection { ...defaultProps }>
        { (item) => <div key={ item.id }>{ item }</div> }
      </Collection>
    );
  });

  const makeRequest = () => ({
    read: jest.fn(),
    setParams: jest.fn().mockReturnThis(),
    ttl: jest.fn().mockReturnThis(),
  });

  it('renders', () => {
    expect(wrapper).toExist();
  });

  it('requests a collection on mount', () => {
    const site = new Site();
    const request = makeRequest();
    jest.spyOn(site, 'req').mockReturnValue(request);

    shallow(<Collection { ...defaultProps } site={ site } />);

    expect(site.req).toHaveBeenCalled();
    expect(site.req).toHaveBeenCalledWith('type-slug');
    expect(request.read).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalled();
    expect(request.setParams).toHaveBeenCalledWith(defaultProps.params);
    expect(request.ttl).toHaveBeenCalled();
    expect(request.ttl).toHaveBeenCalledWith(false);
  });

  it('requests a collection if the params change', () => {
    const params = { new: 'params' };
    const site = new Site();
    const request = makeRequest();
    jest.spyOn(site, 'req').mockReturnValue(request);
    wrapper.setProps({ params, site });

    expect(site.req).toHaveBeenCalled();
    expect(site.req).toHaveBeenCalledWith('type-slug');
    expect(request.setParams).toHaveBeenCalledWith(params);
  });

  it('requests a collection if the slug changes', () => {
    const site = new Site();
    const request = makeRequest();
    jest.spyOn(site, 'req').mockReturnValue(request);

    wrapper.setProps({ site, typeSlug: 'type-slug-2' });

    expect(site.req).toHaveBeenCalled();
    expect(site.req).toHaveBeenCalledWith('type-slug-2');
    expect(request.setParams).toHaveBeenCalledWith(defaultProps.params);
  });

  it('avoids requesting a collection if props are unchanged', () => {
    const site = new Site();
    jest.spyOn(site, 'req');
    const props = { params: { }, site, typeSlug: 'type-slug' };
    wrapper.setProps(props);
    site.req.mockClear();

    wrapper.instance().componentDidUpdate(props);

    expect(site.req).not.toHaveBeenCalled();
  });

  it('avoids requesting a collection if params are unchanged by value', () => {
    const site = new Site();
    jest.spyOn(site, 'req');
    wrapper.setProps({ params: { field: 'value' }, site });
    site.req.mockClear();

    // Different params object (!==) but the same value
    wrapper.setProps({ params: { field: 'value' } });

    expect(site.req).not.toHaveBeenCalled();
  });

  describe('items mode', () => {
    it('calls the provided function with each item', () => {
      const children = jest.fn();
      wrapper.setProps({ children });
      expect(children).toHaveBeenCalled();
      expect(children).toHaveBeenCalledTimes(2);
      expect(children).toHaveBeenCalledWith(COLLECTION_FIXTURE[0]);
      expect(children).toHaveBeenCalledWith(COLLECTION_FIXTURE[1]);
    });

    it('does nothing until the collection is available', () => {
      const children = jest.fn();
      wrapper.setProps({ children, collection: undefined });
      expect(wrapper).toBeEmptyRender();
      expect(children).not.toHaveBeenCalled();
    });

    it('does nothing if the collection is empty', () => {
      const children = jest.fn();
      wrapper.setProps({ children, collection: [] });
      expect(wrapper.html()).toBe('');
      expect(children).not.toHaveBeenCalled();
    });

    it('returns emptyContent if collection is empty', () => {
      const children = jest.fn();
      wrapper.setProps({
        children,
        collection: [],
        emptyContent: <p>Empty</p>,
      });

      expect(wrapper).toContainReact(<p>Empty</p>);
      expect(children).not.toHaveBeenCalled();
    });

    it('renders the result of the provided function', () => {
      expect(wrapper).toContainReact(<div>{ COLLECTION_FIXTURE[0] }</div>);
      expect(wrapper).toContainReact(<div>{ COLLECTION_FIXTURE[1] }</div>);
    });

    it('renders nothing if no function is provided', () => {
      wrapper.setProps({ children: undefined });
      expect(wrapper.html()).toBe('');
    });
  });

  describe('collection mode', () => {
    beforeEach(() => {
      wrapper.setProps({ mode: 'collection' });
    });

    it('calls the provided function with the collection', () => {
      const children = jest.fn();
      wrapper.setProps({ children });
      expect(children).toHaveBeenCalled();
      expect(children).toHaveBeenCalledTimes(1);
      expect(children).toHaveBeenCalledWith(
        COLLECTION_FIXTURE,
        defaultProps.pagination
      );
    });

    it('does nothing until the collection is available', () => {
      const children = jest.fn();
      wrapper.setProps({ children, collection: undefined });
      expect(wrapper).toBeEmptyRender();
      expect(children).not.toHaveBeenCalled();
    });

    it('still calls the function if the collection is empty', () => {
      const children = jest.fn();
      const pagination = { total: 0, totalPages: 0 };
      wrapper.setProps({ children, collection: [], pagination });

      expect(wrapper).toBeEmptyRender();
      expect(children).toHaveBeenCalled();
      expect(children).toHaveBeenCalledWith([], pagination);
    });

    it('renders emptyContent if available and collection is empty', () => {
      const children = jest.fn();

      wrapper.setProps({
        children,
        collection: [],
        emptyContent: <p>Empty</p>,
      });

      expect(wrapper).toContainReact(<p>Empty</p>);
      expect(children).not.toHaveBeenCalled();
    });

    it('renders the result of the provided function', () => {
      expect(wrapper).toContainReact(<div>{ COLLECTION_FIXTURE }</div>);
    });

    it('renders nothing if no function is provided', () => {
      wrapper.setProps({ children: undefined });
      expect(wrapper).toBeEmptyRender();
    });
  });

  it('copes if the mode is unknown', () => {
    wrapper.setProps({ mode: 'not a mode' });
    expect(wrapper).toBeEmptyRender();
  });

  it('can provide pagination data if available on mount', () => {
    shallow(<Collection { ...defaultProps } />);
    expect(onUpdate).toHaveBeenCalled();
    expect(onUpdate).toHaveBeenCalledWith(defaultProps.pagination);
  });

  it('copes if pagination data is not available on mount', () => {
    const props = dissoc('pagination', defaultProps);
    onUpdate.mockClear();

    shallow(<Collection { ...props } />);

    expect(onUpdate).not.toHaveBeenCalled();
  });

  it('copes if the loaded callback is not available on mount', () => {
    const props = dissoc('onUpdate', defaultProps);
    shallow(<Collection { ...props } />);
    // Test succeeds if no error
  });

  it('can provide pagination data if available on update', () => {
    const pagination = assoc('total', 43, defaultProps.pagination);
    onUpdate.mockClear();

    wrapper.setProps({ pagination });

    expect(onUpdate).toHaveBeenCalled();
    expect(onUpdate).toHaveBeenCalledWith(pagination);
  });

  it('copes if the loaded callback is not available on update', () => {
    const pagination = assoc('total', 43, defaultProps.pagination);
    onUpdate.mockClear();
    wrapper.setProps({ onUpdate: undefined, pagination });
    // Test succeeds if no error
  });

  it('calls the loaded callback when provided for the first time', () => {
    const newOnLoaded = jest.fn();
    wrapper.setProps({ onUpdate: newOnLoaded });
    expect(newOnLoaded).toHaveBeenCalled();
    expect(newOnLoaded).toHaveBeenCalledWith(defaultProps.pagination);
  });

  it('avoids calling the loaded callback with the same pagination data', () => {
    onUpdate.mockClear();
    wrapper.setProps({ pagination: { total: 42, totalPages: 5 } });
    expect(onUpdate).not.toHaveBeenCalled();
  });

  it('displays placeholder until the collection is available', () => {
    wrapper.setProps({ collection: undefined, placeholder: <Placeholder /> });
    expect(wrapper).toContainReact(<Placeholder />);
  });

  it('removes the placeholder when the item arrives', () => {
    wrapper.setProps({ placeholder: <Placeholder /> });
    expect(wrapper).not.toContainReact(<Placeholder />);
  });
});
