import { html } from 'lit'
import { assert, nextFrame } from '@open-wc/testing'
import sinon from 'sinon'
import { ApplicationRenderer } from '../../src/core/renderer/ApplicationRenderer.js'
import { type Application } from '../../src/core/Application.js'
import { FragmentRenderer } from '../../src/core/renderer/FragmentRenderer.js'
import { Renderer } from '../../src/core/renderer/Renderer.js'
import { type Fragment } from '../../src/core/Fragment.js'

describe('Renderer', () => {
  class TestRenderer extends Renderer {
    public renderCalled = false
    public renderArgs: unknown[] = []
    protected renderer(): void {
      this.renderCalled = true
      // eslint-disable-next-line prefer-rest-params
      this.renderArgs = [...arguments]
      this.resolveUpdatePromise()
    }
  }

  it('should set renderRoot from HTMLElement', () => {
    const el = document.createElement('div')
    const renderer = new TestRenderer()
    renderer.setRenderRoot(el)
    assert.strictEqual(renderer.renderRoot, el)
  })

  it('should set renderRoot from selector', () => {
    const el = document.createElement('div')
    el.id = 'test-root'
    document.body.appendChild(el)
    const renderer = new TestRenderer()
    renderer.setRenderRoot('#test-root')
    assert.strictEqual(renderer.renderRoot, el)
    document.body.removeChild(el)
  })

  it('should call renderer() on requestUpdate()', () => {
    const renderer = new TestRenderer()
    const spy = sinon.spy(renderer as unknown as { renderer: () => void }, 'renderer')
    renderer.requestUpdate()
    // Wait for animation frame
    return new Promise<void>((resolve) => {
      requestAnimationFrame(() => {
        assert.isTrue(spy.calledOnce)
        resolve()
      })
    })
  })

  it('should not schedule multiple renders if already rendering', async () => {
    const renderer = new TestRenderer()
    renderer.requestUpdate()
    const spy = sinon.spy(renderer as unknown as { renderer: () => void }, 'renderer')
    renderer.requestUpdate()
    assert.isTrue(spy.notCalled)
  })

  it('should clear root with _clearRoot()', () => {
    const el = document.createElement('div')
    el.appendChild(document.createElement('span'))
    const renderer = new TestRenderer()
    renderer['_clearRoot'](el)
    assert.strictEqual(el.childNodes.length, 0)
  })

  it('should resolve updateComplete after _render()', async () => {
    const el = document.createElement('div')
    const renderer = new TestRenderer()
    renderer.setRenderRoot(el)
    renderer.requestUpdate()
    await renderer.updateComplete
    assert.isFalse(renderer.firstRendered) // firstRenderedFlag is not set in TestRenderer
  })
})

describe('ApplicationRenderer', () => {
  it('should call _handleFirstRender and _render', async () => {
    const app = {
      onFirstRender: sinon.spy(),
      onRendered: sinon.spy(),
      manager: { getTopActivity: () => null },
      render: () => html`<div>app</div>`,
    }
    const renderer = new ApplicationRenderer(app as unknown as Application)
    const root = document.createElement('div')
    renderer.setRenderRoot(root)
    const spyRender = sinon.spy(renderer as unknown as { _render: () => void }, '_render')
    renderer['renderer']()
    await nextFrame()
    assert.isTrue(app.onFirstRender.called, 'onFirstRender should be called')
    assert.isTrue(app.onRendered.called, 'onRendered should be called')
    assert.isTrue(spyRender.called, '_render should be called')
  })
})

describe('FragmentRenderer', () => {
  it('should call _clearRoot and _render on first render', async () => {
    const fragment = {
      getSingleVisibleFragment: () => null,
      render: () => html`<div>fragment</div>`,
      onFirstRender: sinon.spy(),
    }
    const renderer = new FragmentRenderer(fragment as unknown as Fragment)
    const root = document.createElement('div')
    renderer.setRenderRoot(root)
    renderer['renderer']()
    await nextFrame()
    assert.isTrue(fragment.onFirstRender.called, 'onFirstRender should be called')
    assert.strictEqual(root.childElementCount, 1, 'Root should have one child after rendering')
  })
})
