import { assert, aTimeout, fixture, html, nextFrame } from '@open-wc/testing'
import sinon from 'sinon'
import { ProjectMock, Property } from '@api-client/core/browser.js'
import { loadMonaco } from '../MonacoSetup.js'
import BodyFormdataEditorElement from '../../../src/elements/http/BodyUrlEncodedEditor.js'
import SwitchElement from '../../../src/md/switch/internals/SwitchElement.js'
import type UiButtonElement from '../../../src/md/icon-button/internals/IconButton.js'
import '../../../src/define/http/http-body-urlencoded-editor.js'

describe('elements', () => {
  describe('http', () => {
    describe('BodyUrlEncodedEditor', () => {
      async function basicFixture(): Promise<BodyFormdataEditorElement> {
        return fixture(html`<http-body-urlencoded-editor allowFormInfo></http-body-urlencoded-editor>`)
      }

      async function valueFixture(value: string): Promise<BodyFormdataEditorElement> {
        return fixture(
          html`<http-body-urlencoded-editor .value="${value}" allowFormInfo></http-body-urlencoded-editor>`
        )
      }

      async function autoEncodeFixture(): Promise<BodyFormdataEditorElement> {
        return fixture(html`<http-body-urlencoded-editor autoEncode allowFormInfo></http-body-urlencoded-editor>`)
      }

      const generator = new ProjectMock()

      before(async () => loadMonaco())

      describe('Empty state', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          element = await basicFixture()
        })

        it('renders empty message', () => {
          const node = element.shadowRoot!.querySelector('.empty-message')
          assert.ok(node)
        })

        it('renders the form mime information', () => {
          const node = element.shadowRoot!.querySelector('.form-info')
          assert.ok(node)
        })

        it('has empty model', () => {
          assert.deepEqual(element.model, [])
        })
      })

      describe('adding a property', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          element = await basicFixture()
        })

        it('adds a model item on add button click', () => {
          element.addParameter()
          assert.lengthOf(element.model, 1)
        })

        it('does not add a model when readonly', () => {
          element.readOnly = true
          element.addParameter()
          assert.lengthOf(element.model, 0)
        })

        it('does not add a model when disabled', () => {
          element.disabled = true
          element.addParameter()
          assert.lengthOf(element.model, 0)
        })

        it('model item has basic schema', () => {
          element.addParameter()
          const p = Property.String()
          assert.deepEqual(element.model[0], p.toJSON())
        })

        it('adds an item with undefined model', () => {
          // @ts-expect-error for testing
          element.model = undefined
          element.addParameter()
          assert.lengthOf(element.model, 1)
        })

        it('focuses on the added item', async () => {
          const spy = sinon.spy(element, 'focusLast')
          element.addParameter()
          await aTimeout(0)
          assert.isTrue(spy.called)
        })

        it('does not notify model change', async () => {
          // there's no point of storing the model without empty values. They are not generating
          // any editor value
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          element.addParameter()
          assert.isFalse(spy.called)
        })
      })

      describe('#value', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          element = await basicFixture()
        })

        it('reads set value', () => {
          element.value = 'a=b'
          assert.equal(element.value, 'a=b')
        })

        it('generates a view model', () => {
          element.value = 'a=b&c=d'
          assert.lengthOf(element.model, 2)
        })

        it('generated model has basic properties', () => {
          element.value = 'a=b&c=d'
          const [item] = element.model
          const p = Property.String('a', 'b')
          assert.deepEqual(item, p.toJSON())
        })

        it('clears the model when no value', () => {
          element.value = 'a=b&c=d'
          assert.lengthOf(element.model, 2)
          element.value = ''
          assert.lengthOf(element.model, 0)
        })

        it('updates existing model', () => {
          const p = Property.String('test', 'true')
          element.model = [p.toJSON()]
          element.value = 'a=b&c=d'
          assert.equal(element.model[0].name, 'a')
        })

        it('does not notify model change', async () => {
          // value/model setters should not dispatch change events
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          element.value = 'a=b&c=d'
          assert.isFalse(spy.called)
        })
      })

      describe('#model', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          element = await basicFixture()
        })

        it('reads set value', () => {
          const p = Property.String('test', 'true')
          element.model = [p.toJSON()]
          assert.deepEqual(element.model, [p.toJSON()])
        })

        it('generates a value', () => {
          const p = Property.String('test', 'true')
          element.model = [p.toJSON()]
          assert.equal(element.value, 'test=true')
        })

        it('clears the previously set value', () => {
          element.value = 'a=b&c=d'
          // @ts-expect-error for testing
          element.model = undefined
          assert.equal(element.value, '')
        })

        it('updates existing value', () => {
          element.value = 'a=b&c=d'
          const p = Property.String('test', 'true')
          element.model = [p.toJSON()]
          assert.equal(element.value, 'test=true')
        })

        it('does not notify model change', () => {
          // value/model setters should not dispatch change events
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const p = Property.String('test', 'true')
          element.model = [p.toJSON()]
          assert.isFalse(spy.called)
        })
      })

      describe('values rendering', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = generator.http.payload.urlEncoded()
          element = await valueFixture(value)
        })

        it('renders form rows for each model entry', () => {
          const items = element.shadowRoot!.querySelectorAll('.param-row')
          assert.lengthOf(items, element.model.length)
        })

        it('renders the enable switch', () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-switch')
          assert.ok(item)
        })

        it('renders the remove button', () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-icon-button')
          assert.ok(item)
        })

        it('renders the name input', () => {
          const item = element.shadowRoot!.querySelector('.param-row .name-input input')
          assert.ok(item)
        })

        it('renders the value input', () => {
          const item = element.shadowRoot!.querySelector('.param-row .value-input input')
          assert.ok(item)
        })
      })

      describe('enable/disable action', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = generator.http.payload.urlEncoded()
          element = await valueFixture(value)
        })

        it('disables the form item', () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          assert.isFalse(element.model[0].enabled)
        })

        it('updates the value', () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          const { name, value: modelValue } = element.model[0]
          const { value } = element
          assert.notInclude(value, `${name}=${modelValue}`)
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          assert.isTrue(spy.called)
        })

        it('re-enables the form item', async () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          await aTimeout(0)
          item.click()
          assert.isTrue(element.model[0].enabled)
        })

        it('changes the value when enabling', async () => {
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          await aTimeout(0)
          item.click()
          const { name, value: modelValue } = element.model[0]
          const { value } = element
          assert.include(value, `${name}=${modelValue}`)
        })

        it('dispatches the change event when enabling', async () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const item = element.shadowRoot!.querySelector('.param-row ui-switch') as SwitchElement
          item.click()
          await aTimeout(0)
          item.click()
          assert.equal(spy.callCount, 2)
        })
      })

      describe('removing an item', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = generator.http.payload.urlEncoded()
          element = await valueFixture(value)
        })

        it('removes an item from the model', () => {
          const item = { ...element.model[0] }
          const button = element.shadowRoot!.querySelector('.param-row ui-icon-button') as UiButtonElement
          button.click()
          assert.notDeepEqual(element.model[0], item)
        })

        it('updates the value', () => {
          const item = { ...element.model[0] }
          const button = element.shadowRoot!.querySelector('.param-row ui-icon-button') as UiButtonElement
          button.click()
          assert.notInclude(element.value, `${item.name}=${item.value}`)
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const button = element.shadowRoot!.querySelector('.param-row ui-icon-button') as UiButtonElement
          button.click()
          assert.isTrue(spy.calledOnce)
        })
      })

      describe('name change', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = generator.http.payload.urlEncoded()
          element = await valueFixture(value)
        })

        it('changes the name', () => {
          const item = { ...element.model[0] }
          const input = element.shadowRoot!.querySelector('.param-row .name-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.notEqual(element.model[0].name, item.name)
          assert.equal(element.model[0].name, 'new-value-test')
        })

        it('updates the value', () => {
          const input = element.shadowRoot!.querySelector('.param-row .name-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.include(element.value, `new-value-test=${element.model[0].value}`)
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const input = element.shadowRoot!.querySelector('.param-row .name-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.isTrue(spy.calledOnce)
        })
      })

      describe('value change', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = generator.http.payload.urlEncoded()
          element = await valueFixture(value)
        })

        it('changes the value', () => {
          const item = { ...element.model[0] }
          const input = element.shadowRoot!.querySelector('.param-row .value-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.notEqual(element.model[0].value, item.value)
          assert.equal(element.model[0].value, 'new-value-test')
        })

        it('updates the value', () => {
          const input = element.shadowRoot!.querySelector('.param-row .value-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.include(element.value, `${element.model[0].name}=new-value-test`)
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          const input = element.shadowRoot!.querySelector('.param-row .value-input input') as HTMLInputElement
          input.value = 'new-value-test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.isTrue(spy.calledOnce)
        })
      })

      describe('value encoding', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = 'a b=c d'
          element = await valueFixture(value)
        })

        it('has unchanged values in the model', () => {
          const [item] = element.model
          assert.equal(item.name, 'a b')
          assert.equal(item.value, 'c d')
        })

        it('encodes the model values', () => {
          element.encodeParameters()
          const [item] = element.model
          assert.equal(item.name, 'a+b')
          assert.equal(item.value, 'c+d')
        })

        it('encodes the value', () => {
          element.encodeParameters()
          assert.equal(element.value, 'a+b=c+d')
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          element.encodeParameters()
          assert.isTrue(spy.calledOnce)
        })
      })

      describe('value decoding', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          const value = 'a+b=c+d'
          element = await valueFixture(value)
        })

        it('has unchanged values in the model', () => {
          const [item] = element.model
          assert.equal(item.name, 'a+b')
          assert.equal(item.value, 'c+d')
        })

        it('decodes the model values', () => {
          element.decodeParameters()
          const [item] = element.model
          assert.equal(item.name, 'a b')
          assert.equal(item.value, 'c d')
        })

        it('decodes the value', () => {
          element.decodeParameters()
          assert.equal(element.value, 'a b=c d')
        })

        it('dispatches the change event', () => {
          const spy = sinon.spy()
          element.addEventListener('change', spy)
          element.decodeParameters()
          assert.isTrue(spy.calledOnce)
        })
      })

      describe('#autoEncode', () => {
        let element: BodyFormdataEditorElement
        beforeEach(async () => {
          element = await autoEncodeFixture()
        })

        it('automatically decodes the set value', () => {
          element.value = 'a+b=c+d'
          const [item] = element.model
          assert.equal(item.name, 'a b')
          assert.equal(item.value, 'c d')
        })

        it('encodes values on input change', async () => {
          element.value = 'a+b=c+d'
          await nextFrame()
          const input = element.shadowRoot!.querySelector('.param-row .name-input input') as HTMLInputElement
          input.value = 'new value test'
          input.dispatchEvent(new CustomEvent('change'))
          assert.equal(element.value, 'new+value+test=c+d')
        })

        it('does not double encodes values', async () => {
          element.value = 'a+b=c+d'
          await nextFrame()
          const input = element.shadowRoot!.querySelector('.param-row .name-input input') as HTMLInputElement
          input.value = 'new value test'
          input.dispatchEvent(new CustomEvent('change'))
          await nextFrame()
          input.value = 'new value tests'
          input.dispatchEvent(new CustomEvent('change'))
          assert.equal(element.value, 'new+value+tests=c+d')
        })
      })
    })
  })
})
