/* eslint-env mocha */
/** @jsx createElement */
import { render } from 'inferno'
import Component from 'inferno-component'
import createElement from 'inferno-create-element'
import assert from 'assert'
import { state, signal, props } from 'cerebral/tags'
import { Controller, Compute, Module } from 'cerebral'
import { Container, StateContainer, connect, decorator } from './'

describe('Inferno', () => {
  let container, $, mount
  before(() => {
    container = document.createElement('div')
    document.body.appendChild(container)
    // console.log(scratch)
    $ = s => container.querySelector(s)
    mount = jsx => {
      return render(jsx, container)
    }
  })
  afterEach(() => {
    mount(null)
    container.innerHTML = ''
  })
  after(() => {
    document.body.removeChild(container)
    container = null
  })
  describe('state container', () => {
    it('should be able to wrap app with container', () => {
      class TestComponent extends Component {
        render() {
          return <div />
        }
      }
      const tree = mount(
        <StateContainer>
          <TestComponent />
        </StateContainer>
      )
      assert.ok(tree._lastInput.children instanceof TestComponent)
    })
    it('should be able to expose state', () => {
      const model = {
        foo: 'bar',
      }
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        props => {
          return <div>{props.foo}</div>
        }
      )
      mount(
        <StateContainer state={model}>
          <TestComponent />
        </StateContainer>
      )

      assert.equal($('div').innerHTML, 'bar')
    })
    it('should be able to expose signals', () => {
      const model = {
        foo: 'bar',
      }
      const signals = {
        test2: [() => {}],
      }
      const TestComponent = connect(
        {
          foo: state`foo`,
          test: signal`test`,
          test2: signal`test2`,
        },
        props => {
          assert.equal(typeof props.test === 'function', true)
          assert.equal(typeof props.test2 === 'object', true)
          return <div>{props.foo}</div>
        }
      )
      mount(
        <StateContainer state={model} signals={signals}>
          <TestComponent />
        </StateContainer>
      )

      assert.equal($('div').innerHTML, 'bar')
    })
    it('should be able to expose state with connectDecorator', () => {
      const model = {
        foo: 'bar',
      }
      const TestComponent = decorator({
        foo: state`foo`,
      })(props => {
        return <div>{props.foo}</div>
      })
      mount(
        <StateContainer state={model}>
          <TestComponent />
        </StateContainer>
      )

      assert.equal($('div').innerHTML, 'bar')
    })
  })
  describe('container', () => {
    it('should be able to expose controller', () => {
      const controller = Controller(
        Module({
          state: {
            foo: 'bar',
          },
        })
      )
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        props => {
          return <div>{props.foo}</div>
        }
      )
      mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )

      assert.equal($('div').innerHTML, 'bar')
    })
    it('should throw when no controller provided', () => {
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        props => {
          return <div>{props.foo}</div>
        }
      )
      assert.throws(
        () => {
          mount(
            <Container>
              <TestComponent />
            </Container>
          )
        },
        err => {
          if (err instanceof Error) {
            return (
              err.message ===
              'Cerebral - You are not passing controller to Container'
            )
          }
        }
      )
    })
    it('should throw when container component is not provided', () => {
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        props => {
          return <div>{props.foo}</div>
        }
      )
      assert.throws(
        () => {
          mount(<TestComponent />)
        },
        err => {
          if (err instanceof Error) {
            return (
              err.message ===
              'Cerebral - Can not find controller, did you remember to use the Container component? Read more at: https://cerebraljs.com/docs/views/inferno.html'
            )
          }
        }
      )
    })
    it('should be able to unregister component from container after unmounting component', () => {
      const controller = Controller(
        Module({
          state: {
            foo: 'bar',
          },
        }),
        {
          devtools: { init() {}, send() {}, updateComponentsMap() {} },
        }
      )
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        props => {
          return <div>{props.foo}</div>
        }
      )
      const tree = mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )
      assert.equal(tree._lastInput.children.view.isUnmounted, false)
      assert.equal($('div').innerHTML, 'bar')
      mount(null)
      assert.equal(tree._lastInput.children.view.isUnmounted, true)
      assert.deepEqual(
        controller.componentDependencyStore.getAllUniqueEntities(),
        []
      )
    })
  })
  describe('connect', () => {
    it('should convert component to json', () => {
      const controller = Controller(
        Module({
          state: {
            foo: 'bar',
          },
        })
      )
      const MyComponent = props => {
        return <div>{props.foo}</div>
      }
      MyComponent.displayName = 'Test'
      const TestComponent = connect(
        {
          foo: state`foo`,
        },
        MyComponent
      )
      const tree = mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )
      assert.equal(tree._lastInput.children.toJSON(), 'Test')
    })
    it('should render ones where multiple state matches', () => {
      const controller = Controller(
        Module({
          state: {
            foo: 'bar',
            bar: 'foo',
          },
          signals: {
            test: [
              ({ state }) => {
                state.set('foo', 'bar2')
                state.set('bar', 'foo2')
              },
            ],
          },
        })
      )
      let renderCount = 0
      const TestComponent = connect(
        {
          foo: state`foo`,
          bar: state`bar`,
        },
        props => {
          renderCount++
          return <div>{props.foo}</div>
        }
      )
      mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )

      assert.equal($('div').innerHTML, 'bar')
      assert.equal(renderCount, 1)
      controller.getSignal('test')()
      assert.equal(renderCount, 2)
    })
    it('should rerender affected components', () => {
      let renderCount = 0
      const controller = Controller(
        Module({
          state: {
            foo: 'bar',
          },
          signals: {
            methodCalled: [({ state }) => state.set('foo', 'bar2')],
          },
        })
      )
      class TestComponentClass2 extends Component {
        render() {
          renderCount++
          return <div />
        }
      }
      const TestComponent2 = connect({}, TestComponentClass2)
      class TestComponentClass extends Component {
        callSignal() {
          this.props.methodCalled()
        }
        render() {
          renderCount++
          return <div><TestComponent2 /></div>
        }
      }
      const TestComponent = connect(
        {
          foo: state`foo`,
          methodCalled: signal`methodCalled`,
        },
        TestComponentClass
      )
      const tree = mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )
      const component = tree._lastInput.children._lastInput.children
      component.callSignal()
      assert.equal(renderCount, 3)
    })
    it('should allow props tag and computed', () => {
      const controller = Controller(
        Module({
          state: {
            foo: {
              baz: 'mip',
            },
          },
        })
      )
      const TestComponent = connect(
        {
          a: Compute(props`foo`),
          b: props`bar.id`,
          c: state`foo.${props`propKey`}`,
        },
        ({ a, b, c }) => {
          return <div>{a + b + c}</div>
        }
      )
      mount(
        <Container controller={controller}>
          <TestComponent foo="bar" bar={{ id: '1' }} propKey="baz" />
        </Container>
      )
      assert.equal($('div').innerHTML, 'bar1mip')
    })
    it('should update on props change', () => {
      const controller = Controller(Module({}))
      class TestComponentClass2 extends Component {
        render() {
          return <div>{this.props.foo}</div>
        }
      }
      const TestComponent2 = connect({}, TestComponentClass2)
      class TestComponentClass extends Component {
        constructor(props) {
          super(props)
          this.state = { foo: 'bar' }
        }
        changePath() {
          this.setState({
            foo: 'bar2',
          })
        }
        render() {
          return <span><TestComponent2 foo={this.state.foo} /></span>
        }
      }
      const TestComponent = connect({}, TestComponentClass)
      const tree = mount(
        <Container controller={controller}>
          <TestComponent />
        </Container>
      )
      assert.equal($('div').innerHTML, 'bar')
      const component = tree._lastInput.children._lastInput.children
      component.changePath()
      assert.equal($('div').innerHTML, 'bar2')
    })
  })
})
