import { assert } from '@esm-bundle/chai'
import sinon from 'sinon'
import { fixture, html } from '@open-wc/testing'
import { Application, type UpdateRequest } from '../../src/core/Application.js'
import { Activity } from '../../src/core/Activity.js'
import { ActivityManager, ActivityLifecycle } from '../../src/core/ActivityManager.js'
import { ApplicationRenderer } from '../../src/core/renderer/ApplicationRenderer.js'
import { ThemeManager } from '../../src/core/ThemeManager.js'
import { nothing } from 'lit'

class TestActivity extends Activity {
  static override action = 'test-activity'
  renderSpy = sinon.spy(super.render)
  override render() {
    this.renderSpy()
    return html`<div>Test Activity Content</div>`
  }
}

class TestApplication extends Application {
  onStartSpy = sinon.stub().resolves()
  onStopSpy = sinon.stub().resolves()
  onFirstRenderSpy = sinon.spy(super.onFirstRender)
  onRenderedSpy = sinon.spy(super.onRendered)

  override async onStart(): Promise<void> {
    this.manager.registerActivity(TestActivity.action, TestActivity)
    return this.onStartSpy()
  }

  override async onStop(): Promise<void> {
    return this.onStopSpy()
  }

  override onFirstRender(): void {
    // super.onFirstRender()
    this.onFirstRenderSpy()
  }

  override onRendered(): void {
    // super.onRendered()
    this.onRenderedSpy()
  }
}

async function basicFixture(): Promise<HTMLElement> {
  return fixture(html`<div id="app-root"></div>`)
}

describe('Application', () => {
  let renderRootElement: HTMLElement
  let app: TestApplication

  beforeEach(async () => {
    renderRootElement = await basicFixture()
    app = new TestApplication(renderRootElement)
  })

  afterEach(() => {
    sinon.restore()
  })

  describe('Constructor and Getters', () => {
    it('initializes ActivityManager', () => {
      assert.instanceOf(app.manager, ActivityManager)
    })

    it('initializes ApplicationRenderer and sets render root', () => {
      assert.instanceOf(app.renderer, ApplicationRenderer)
      assert.strictEqual(app.renderer.renderRoot, renderRootElement)
    })

    it('initializes ThemeManager', () => {
      assert.instanceOf(app.theme, ThemeManager)
    })

    it('initializes events EventTarget', () => {
      assert.instanceOf(app.events, EventTarget)
    })

    it('adds activity:update listener to app.events', () => {
      const spy = sinon.spy(app, 'requestUpdate')
      app.events.dispatchEvent(new CustomEvent('activity:update', { detail: {} }))
      assert.isTrue(spy.calledOnce)
    })

    it('getter "updateComplete" delegates to renderer', () => {
      const promise = Promise.resolve()
      sinon.stub(app.renderer, 'updateComplete').get(() => promise)
      assert.strictEqual(app.updateComplete, promise)
    })

    it('getter "firstRendered" delegates to renderer', () => {
      sinon.stub(app.renderer, 'firstRendered').get(() => true)
      assert.isTrue(app.firstRendered)
    })
  })

  describe('setRenderRoot()', () => {
    it('calls renderer.setRenderRoot', async () => {
      const newRoot = (await fixture(html`<div></div>`)) as HTMLElement
      const spy = sinon.spy(app.renderer, 'setRenderRoot')
      app.setRenderRoot(newRoot)
      assert.isTrue(spy.calledOnceWith(newRoot))
    })
  })

  describe('Lifecycle Methods', () => {
    it('onFirstRender dispatches app:first-render event', () => {
      const eventSpy = sinon.spy()
      app.events.addEventListener('app:first-render', eventSpy)
      app.onFirstRender()
      assert.isTrue(eventSpy.calledOnce, 'app:first-render event should be dispatched')
      assert.isTrue(app.onFirstRenderSpy.calledOnce, 'onFirstRender should be called')
    })

    it('onRendered can be called', () => {
      app.onRendered()
      assert.isTrue(app.onRenderedSpy.calledOnce)
    })
  })

  describe('render()', () => {
    it('returns nothing if no current activity', () => {
      assert.strictEqual(app.render(), nothing)
    })

    it('returns nothing if current activity is not started', async () => {
      app.manager.registerActivity(TestActivity.action, TestActivity)
      await app.manager.createActivity({ action: TestActivity.action })
      // Activity is created but not started
      assert.strictEqual(app.render(), nothing)
    })

    it('calls render on the current activity if it is started or resumed', async () => {
      await app.run() // This calls onStart which registers TestActivity
      await app.manager.startActivity({ action: TestActivity.action })
      const activity = app.getCurrentActivity() as TestActivity
      activity.lifecycle = ActivityLifecycle.Started // Ensure state

      app.render()
      assert.isTrue(activity.renderSpy.calledOnce)

      activity.lifecycle = ActivityLifecycle.Resumed // Ensure state
      activity.renderSpy.resetHistory()
      app.render()
      assert.isTrue(activity.renderSpy.calledOnce)
    })
  })

  describe('requestUpdate()', () => {
    it('calls renderer.requestUpdate by default', () => {
      const spy = sinon.spy(app.renderer, 'requestUpdate')
      app.requestUpdate()
      assert.isTrue(spy.calledOnce)
    })

    it('does not call renderer.requestUpdate if app:false option is passed', () => {
      const spy = sinon.spy(app.renderer, 'requestUpdate')
      app.requestUpdate({ app: false })
      assert.isFalse(spy.called)
    })

    it('is called when activity:update event is dispatched on app.events', () => {
      const requestUpdateSpy = sinon.spy(app, 'requestUpdate')
      const detail: UpdateRequest = { activity: false }
      app.events.dispatchEvent(new CustomEvent('activity:update', { detail }))
      assert.isTrue(requestUpdateSpy.calledOnceWith(detail))
    })
  })

  describe('run()', () => {
    it('calls onStart', async () => {
      await app.run()
      assert.isTrue(app.onStartSpy.calledOnce)
    })

    it('calls requestUpdate', async () => {
      const spy = sinon.spy(app, 'requestUpdate')
      await app.run()
      assert.isTrue(spy.calledOnce)
    })
  })

  describe('stop()', () => {
    it('removes activity:update listener', async () => {
      const requestUpdateSpy = sinon.spy(app, 'requestUpdate')
      await app.stop()
      app.events.dispatchEvent(new CustomEvent('activity:update')) // Should not call requestUpdate
      assert.isFalse(requestUpdateSpy.called)
    })

    it('calls onStop', async () => {
      await app.stop()
      assert.isTrue(app.onStopSpy.calledOnce)
    })
  })

  describe('Activity Accessors', () => {
    it('getCurrentActivity delegates to ActivityManager', () => {
      const activity = new TestActivity(app)
      const stub = sinon.stub(app.manager, 'getCurrentActivity').returns(activity)
      assert.strictEqual(app.getCurrentActivity(), activity)
      assert.isTrue(stub.calledOnce)
    })

    it('getApplication returns this', () => {
      assert.strictEqual(app.getApplication(), app)
    })

    it('findActiveActivity delegates to ActivityManager', () => {
      const activity = new TestActivity(app)
      const stub = sinon.stub(app.manager, 'findActiveActivity').returns(activity)
      assert.strictEqual(app.findActiveActivity('test-id'), activity)
      assert.isTrue(stub.calledOnceWith('test-id'))
    })
  })
})
