import { assert } from '@esm-bundle/chai'
import sinon from 'sinon'
import { fixture, html } from '@open-wc/testing'
import { Fragment } from '../../src/core/Fragment.js'
import { Activity } from '../../src/core/Activity.js'
import { Application } from '../../src/core/Application.js'
import { FragmentState, FragmentManager } from '../../src/core/FragmentManager.js'
import { IntentResult, type Intent, ActivityLifecycle } from '../../src/core/ActivityManager.js'
import { FragmentRenderer } from '../../src/core/renderer/FragmentRenderer.js'
import { EventTypes } from '../../src/events/EventTypes.js'
import { nothing } from 'lit'
import type { ActivityDetail } from '../../src/events/IntentEvents.js'

class TestActivity extends Activity {
  static override action = 'test-activity'
  startActivitySpy = sinon.spy(super.startActivity)
  startActivityForResultSpy = sinon.stub().resolves(123) // Returns a mock request code
  requestUpdateSpy = sinon.spy(super.requestUpdate)

  override async startActivity(intent: Intent): Promise<void> {
    return this.startActivitySpy(intent)
  }

  override async startActivityForResult(intent: Intent): Promise<number> {
    return this.startActivityForResultSpy(intent)
  }

  override requestUpdate(): void {
    this.requestUpdateSpy()
  }
}

class TestApplication extends Application {
  override async onStart(): Promise<void> {
    this.manager.registerActivity(TestActivity.action, TestActivity)
  }

  requestUpdateSpy = sinon.spy(super.requestUpdate)
  override requestUpdate(): void {
    this.requestUpdateSpy()
  }
}

class TestFragment extends Fragment {
  onCreateSpy = sinon.spy(super.onCreate)
  onAttachSpy = sinon.spy(super.onAttach)
  onStartSpy = sinon.spy(super.onStart)
  onResumeSpy = sinon.spy(super.onResume)
  onPauseSpy = sinon.spy(super.onPause)
  onStopSpy = sinon.spy(super.onStop)
  onDetachSpy = sinon.spy(super.onDetach)
  onDestroySpy = sinon.spy(super.onDestroy)
  onDataSpy = sinon.spy(super.onData)
  onFirstRenderSpy = sinon.spy(super.onFirstRender)
  renderSpy = sinon.spy(super.render)
  onActivityResultSpy = sinon.spy(super.onActivityResult)

  override onCreate(data?: unknown) {
    return this.onCreateSpy(data)
  }
  override onAttach(data?: unknown) {
    return this.onAttachSpy(data)
  }
  override onStart() {
    return this.onStartSpy()
  }
  override onResume() {
    return this.onResumeSpy()
  }
  override onPause() {
    return this.onPauseSpy()
  }
  override onStop() {
    return this.onStopSpy()
  }
  override onDetach() {
    return this.onDetachSpy()
  }
  override onDestroy() {
    return this.onDestroySpy()
  }
  override onData(data: unknown) {
    return this.onDataSpy(data)
  }
  override onFirstRender() {
    return this.onFirstRenderSpy()
  }
  override render() {
    return this.renderSpy()
  }
  override async onActivityResult(requestCode: number, resultCode: IntentResult, intent: Intent) {
    return this.onActivityResultSpy(requestCode, resultCode, intent)
  }
}

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

describe('Fragment', () => {
  let app: TestApplication
  let activity: TestActivity
  let fragment: TestFragment

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    app = new TestApplication(renderRoot)
    await app.run() // To register activity
    activity = new TestActivity(app) // Parent for the fragment
    activity.lifecycle = ActivityLifecycle.Resumed // Mock activity as active
    fragment = new TestFragment({ parent: activity })
  })

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

  describe('Constructor and Initial State', () => {
    it('initializes with state Initialized', () => {
      assert.equal(fragment.state, FragmentState.Initialized)
    })

    it('sets parent if provided', () => {
      assert.strictEqual(fragment.parent, activity)
    })

    it('initializes FragmentManager', () => {
      assert.instanceOf(fragment['fragmentManager'], FragmentManager)
    })

    it('initializes FragmentRenderer', () => {
      assert.instanceOf(fragment.renderer, FragmentRenderer)
    })

    it('initializes requestCode to -1', () => {
      assert.equal(fragment.requestCode, -1)
    })

    it('initializes pendingActivityResult as an empty Map', () => {
      assert.instanceOf(fragment.pendingActivityResult, Map)
      assert.equal(fragment.pendingActivityResult.size, 0)
    })
  })

  describe('Lifecycle Methods and Events', () => {
    const lifecycleMethods: (keyof TestFragment & keyof Fragment)[] = [
      'onCreate',
      'onAttach',
      'onStart',
      'onResume',
      'onPause',
      'onStop',
      'onDetach',
      'onDestroy',
    ]
    lifecycleMethods.forEach((methodName) => {
      it(`calls ${methodName} and dispatches fragment:${methodName.substring(2).toLowerCase()} event`, async () => {
        const eventSpy = sinon.spy()
        const eventName = `fragment:${methodName.substring(2).toLowerCase()}`
        fragment.addEventListener(eventName, eventSpy)

        const data = { test: 'data' }
        if (methodName === 'onCreate' || methodName === 'onAttach') {
          await fragment[methodName](data)
          assert.isTrue((fragment[`${methodName}Spy`] as sinon.SinonSpy).calledOnceWith(data))
          assert.isTrue(eventSpy.calledOnce)
          assert.deepEqual(eventSpy.firstCall.args[0].detail, data)
        } else {
          // @ts-expect-error Testing dynamic method calls
          await fragment[methodName]()
          // @ts-expect-error Testing dynamic method calls
          assert.isTrue((fragment[`${methodName}Spy`] as sinon.SinonSpy).calledOnce)
          assert.isTrue(eventSpy.calledOnce)
        }
      })
    })

    it('calls fragmentManager.onDestroy in onDestroy', async () => {
      const managerDestroySpy = sinon.spy(fragment['fragmentManager'], 'onDestroy')
      await fragment.onDestroy()
      assert.isTrue(managerDestroySpy.calledOnce)
    })

    it('calls onData and dispatches fragment:data event', () => {
      const eventSpy = sinon.spy()
      fragment.addEventListener('fragment:data', eventSpy)
      const data = { info: 'test' }
      fragment.onData(data)
      assert.isTrue(fragment.onDataSpy.calledOnceWith(data))
      assert.isTrue(eventSpy.calledOnce)
      assert.deepEqual(eventSpy.firstCall.args[0].detail, data)
    })

    it('onFirstRender can be called', () => {
      fragment.onFirstRender()
      assert.isTrue(fragment.onFirstRenderSpy.calledOnce)
    })
  })

  describe('Rendering', () => {
    it('render returns nothing by default', () => {
      assert.strictEqual(fragment.render(), nothing)
      assert.isTrue(fragment.renderSpy.calledOnce)
    })

    it('render calls render on single visible child fragment', () => {
      const childFragment = new TestFragment()
      const childRenderSpy = sinon.spy(childFragment, 'render')
      sinon.stub(fragment, 'getSingleVisibleFragment').returns(childFragment)

      fragment.render()
      assert.isTrue(childRenderSpy.calledOnce)
    })

    it('getSingleVisibleFragment returns fragment if only one is visible', () => {
      const child = new TestFragment()
      sinon.stub(fragment['fragmentManager'], 'getVisible').returns([child])
      assert.strictEqual(fragment.getSingleVisibleFragment(), child)
    })

    it('getSingleVisibleFragment returns null if no fragments are visible', () => {
      sinon.stub(fragment['fragmentManager'], 'getVisible').returns([])
      assert.isNull(fragment.getSingleVisibleFragment())
    })

    it('getSingleVisibleFragment returns null if multiple fragments are visible', () => {
      sinon.stub(fragment['fragmentManager'], 'getVisible').returns([new TestFragment(), new TestFragment()])
      assert.isNull(fragment.getSingleVisibleFragment())
    })

    it('isRenderingFragments returns true if children map is not empty', () => {
      fragment['children'].set('child1', new TestFragment())
      assert.isTrue(fragment.isRenderingFragments())
    })

    it('isRenderingFragments returns false if children map is empty', () => {
      assert.isFalse(fragment.isRenderingFragments())
    })

    it('getVisibleFragments delegates to fragmentManager', () => {
      const managerSpy = sinon.spy(fragment['fragmentManager'], 'getVisible')
      fragment.getVisibleFragments()
      assert.isTrue(managerSpy.calledOnce)
    })
  })

  describe('Child Fragment Management', () => {
    it('addChild adds to children map and calls fragmentManager.attachFragment', async () => {
      const child = new TestFragment()
      const managerSpy = sinon.spy(fragment['fragmentManager'], 'attachFragment')
      const data = { key: 'value' }
      await fragment.addChild('childKey', child, data)

      assert.strictEqual(fragment['children'].get('childKey'), child)
      assert.isTrue(managerSpy.calledOnceWith('childKey', child, fragment, data))
    })

    it('removeChild removes from children map and calls fragmentManager.detachFragment', async () => {
      const child = new TestFragment()
      await fragment.addChild('childKey', child)
      const managerSpy = sinon.spy(fragment['fragmentManager'], 'detachFragment')

      await fragment.removeChild('childKey')

      assert.isFalse(fragment['children'].has('childKey'))
      assert.isTrue(managerSpy.calledOnceWith('childKey'))
    })

    it('removeChild does nothing if child not found', async () => {
      const managerSpy = sinon.spy(fragment['fragmentManager'], 'detachFragment')
      await fragment.removeChild('nonExistentKey')
      assert.isFalse(managerSpy.called)
    })
  })

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

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

    it('calls parent.requestUpdate', () => {
      const parentRequestUpdateSpy = sinon.spy(fragment.parent!, 'requestUpdate')
      const opts = { app: false }
      fragment.requestUpdate(opts)
      assert.isTrue(parentRequestUpdateSpy.calledOnceWith(opts))
    })

    it('does not throw if parent is undefined', () => {
      fragment.parent = undefined
      assert.doesNotThrow(() => fragment.requestUpdate())
    })
  })

  describe('Hierarchy Accessors', () => {
    it('getApplication delegates to parent', () => {
      sinon.stub(activity, 'getApplication').returns(app)
      assert.strictEqual(fragment.getApplication(), app)
    })

    it('getActivity delegates to parent', () => {
      sinon.stub(activity, 'getActivity').returns(activity)
      assert.strictEqual(fragment.getActivity(), activity)
    })
  })

  describe('Activity Interaction', () => {
    it('startActivity calls activity.startActivity', async () => {
      const intent = { action: TestActivity.action }
      await fragment.startActivity(intent)
      assert.isTrue(activity.startActivitySpy.calledOnceWith(intent))
    })

    it('startActivity throws if no activity', async () => {
      fragment.parent = undefined
      try {
        await fragment.startActivity({ action: 'test' })
        assert.fail('Should have thrown')
      } catch (e) {
        assert.equal((e as Error).message, 'The fragment has no activity. Unable to start an intent.')
      }
    })

    it('startActivityForResult calls activity.startActivityForResult and sets requestCode', async () => {
      const intent = { action: 'test-action-for-result' }
      await fragment.startActivityForResult(intent)
      assert.isTrue(activity.startActivityForResultSpy.calledOnceWith(intent))
      assert.equal(fragment.requestCode, 123) // Mocked return value
    })

    it('startActivityForResult throws if no activity', async () => {
      fragment.parent = undefined
      try {
        await fragment.startActivityForResult({ action: 'test' })
        assert.fail('Should have thrown')
      } catch (e) {
        assert.equal((e as Error).message, 'The fragment has no activity. Unable to start an intent.')
      }
    })
  })

  describe('onActivityResult()', () => {
    it('calls onResult from pendingActivityResult and removes it', async () => {
      const requestCode = 123
      const onResultSpy = sinon.spy()
      fragment.pendingActivityResult.set(requestCode, { onResult: onResultSpy })
      const intent = { action: 'result-action' }
      await fragment.onActivityResult(requestCode, IntentResult.RESULT_OK, intent)

      assert.isTrue(onResultSpy.calledOnceWith(IntentResult.RESULT_OK, intent))
      assert.isFalse(fragment.pendingActivityResult.has(requestCode))
    })

    it('forwards to child fragment if child has request code', async () => {
      const childFragment = new TestFragment()
      const childOnActivityResultSpy = sinon.spy(childFragment, 'onActivityResult')
      sinon.stub(childFragment, 'hasRequestCode').returns(true)
      fragment.addChild('childKey', childFragment)

      const requestCode = 456
      const intent = { action: 'child-result' }
      await fragment.onActivityResult(requestCode, IntentResult.RESULT_CANCELED, intent)

      assert.isTrue(childOnActivityResultSpy.calledOnceWith(requestCode, IntentResult.RESULT_CANCELED, intent))
    })

    it('resets requestCode and logs if not handled and is base Fragment', async () => {
      const consoleSpy = sinon.spy(console, 'info')
      fragment.requestCode = 789
      // Ensure the fragment instance is exactly Fragment, not a subclass that might override onActivityResult
      Object.setPrototypeOf(fragment, Fragment.prototype)

      await fragment.onActivityResult(789, IntentResult.RESULT_OK, { action: 'test-intent' })

      assert.equal(fragment.requestCode, -1)
      assert.isTrue(
        consoleSpy.calledWith('Fragment#onActivityResult() not implemented', 789, IntentResult.RESULT_OK, {
          action: 'test-intent',
        })
      )
      consoleSpy.restore()
    })
  })

  describe('hasRequestCode()', () => {
    it('returns true if own requestCode matches', () => {
      fragment.requestCode = 123
      assert.isTrue(fragment.hasRequestCode(123))
    })

    it('returns true if a child fragment has the request code', () => {
      const childFragment = new TestFragment()
      sinon.stub(childFragment, 'hasRequestCode').returns(true)
      fragment.addChild('childKey', childFragment)
      assert.isTrue(fragment.hasRequestCode(456))
    })

    it('returns false if neither self nor children have the request code', () => {
      fragment.requestCode = 123
      const childFragment = new TestFragment()
      sinon.stub(childFragment, 'hasRequestCode').returns(false)
      fragment.addChild('childKey', childFragment)
      assert.isFalse(fragment.hasRequestCode(789))
    })
  })

  describe('handleIntentEvent()', () => {
    it('handles startActivityForResult event, calls activity, sets requestCode and pending result', async () => {
      const onResultSpy = sinon.spy()
      const detail = { intent: { action: TestActivity.action }, onResult: onResultSpy }
      const event = new CustomEvent(EventTypes.Intent.startActivityForResult, { detail })

      await fragment.handleIntentEvent(event)

      assert.isTrue(activity.startActivityForResultSpy.calledOnceWith(detail.intent))
      assert.equal(fragment.requestCode, 123) // Mocked return
      assert.isTrue(fragment.pendingActivityResult.has(123))
      assert.strictEqual(fragment.pendingActivityResult.get(123)?.onResult, onResultSpy)
    })

    it('handles startActivity event, calls activity', async () => {
      const detail = { intent: { action: TestActivity.action } }
      const event = new CustomEvent(EventTypes.Intent.startActivity, { detail })

      await fragment.handleIntentEvent(event)
      assert.isTrue(activity.startActivitySpy.calledOnceWith(detail.intent))
    })

    it('throws error if no activity for startActivityForResult', async () => {
      fragment.parent = undefined
      const event = new CustomEvent(EventTypes.Intent.startActivityForResult, {
        detail: {} as unknown as ActivityDetail,
      })
      try {
        await fragment.handleIntentEvent(event)
        assert.fail('Should have thrown')
      } catch (e) {
        assert.equal((e as Error).message, 'The fragment has no activity. Unable to start an intent.')
      }
    })

    it('throws error for unrecognized event type', async () => {
      const event = new CustomEvent('unknown-event', { detail: {} as unknown as ActivityDetail })
      try {
        await fragment.handleIntentEvent(event)
        assert.fail('Should have thrown')
      } catch (e) {
        assert.equal((e as Error).message, `Unrecognized intent event: unknown-event`)
      }
    })
  })

  describe('Misc', () => {
    it('rootClass returns undefined by default', () => {
      assert.isUndefined(fragment.rootClass())
    })

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