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

class TestActivity extends Activity {
  static override action = 'test-activity'
}

class TestActivity2 extends Activity {
  static override action = 'test-activity2'
}

class TestApplication extends Application {
  override async onStart(): Promise<void> {
    this.manager.registerActivity(TestActivity.action, TestActivity)
    this.manager.registerActivity(TestActivity2.action, TestActivity2)
    // The creation below is for tests that require an activity to be present in the manager
    // e.g. for setResult, getApplication, getActivity, hasRequestCode, onActivityResult
    // For other tests, a new TestActivity is created directly.
    // This helps isolate tests for lifecycle events from manager interactions.
    await this.manager.createActivity({ action: TestActivity.action, data: { test: true } })
  }
}

class TestFragment extends Fragment {}

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

describe('Activity Lifecycle and Events', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    activity = new TestActivity(application)
    // Note: application.onStart() is not called here to isolate Activity's own lifecycle methods.
  })

  it('dispatches the activity:create event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:create', spy)
    await activity.onCreate()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:start event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:start', spy)
    await activity.onStart()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:resume event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:resume', spy)
    await activity.onResume()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:pause event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:pause', spy)
    await activity.onPause()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:stop event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:stop', spy)
    await activity.onStop()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:restart event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:restart', spy)
    await activity.onRestart()
    assert.isTrue(spy.calledOnce)
  })

  it('dispatches the activity:destroy event', async () => {
    const spy = sinon.spy()
    activity.addEventListener('activity:destroy', spy)
    await activity.onDestroy()
    assert.isTrue(spy.calledOnce)
  })

  it('calls manager.onDestroy when activity is destroyed', async () => {
    const spy = sinon.spy(activity['manager'], 'onDestroy')
    await activity.onDestroy()
    assert.isTrue(spy.calledOnce)
  })

  it('sets the default state', async () => {
    assert.equal(activity.lifecycle, ActivityLifecycle.Initialized)
  })

  it('sets the default resultCode', async () => {
    assert.equal(activity.resultCode, IntentResult.RESULT_CANCELED)
  })

  it('onNewIntent can be called', async () => {
    assert.doesNotThrow(async () => {
      await activity.onNewIntent({ action: 'test' })
    })
  })

  it('onFirstRender can be called', () => {
    assert.doesNotThrow(() => {
      activity.onFirstRender()
    })
  })

  it('render returns nothing by default', () => {
    assert.strictEqual(activity.render(), nothing)
  })
})

describe('setResult()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // application.onStart() creates an activity instance via the manager
    await application.onStart()
    activity = application.manager.findActiveActivity(TestActivity.action) as Activity
  })

  it('sets the resultData and resultCode', () => {
    const data = { key: 'value' }
    activity.setResult(IntentResult.RESULT_OK, data)
    assert.equal(activity.resultCode, IntentResult.RESULT_OK)
    assert.deepEqual(activity.getResult(), data)
  })
})

describe('getResult()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    activity = new TestActivity(application) // Test with a fresh activity not from manager
  })

  it('returns undefined by default', () => {
    assert.isUndefined(activity.getResult())
  })
})

describe('getApplication()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // application.onStart() creates an activity instance via the manager
    await application.onStart()
    activity = application.manager.findActiveActivity(TestActivity.action) as Activity
  })

  it('returns parent application for getApplication()', () => {
    assert.isTrue(activity.getApplication() === application)
  })
})

describe('getActivity()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // application.onStart() creates an activity instance via the manager
    await application.onStart()
    activity = application.manager.findActiveActivity(TestActivity.action) as Activity
  })

  it('returns self for getActivity()', () => {
    assert.isTrue(activity.getActivity() === activity)
  })
})

describe('hasRequestCode()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // application.onStart() creates an activity instance via the manager
    await application.onStart()
    activity = application.manager.findActiveActivity(TestActivity.action) as Activity
  })

  it('returns true when request code is pending', async () => {
    const requestCode = await activity.startActivityForResult({ action: TestActivity.action })
    assert.isTrue(activity.hasRequestCode(requestCode))
  })

  it('returns false when request code is not pending', async () => {
    await activity.startActivityForResult({ action: TestActivity.action })
    assert.isFalse(activity.hasRequestCode(1))
  })
})

describe('onActivityResult()', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // application.onStart() creates an activity instance via the manager
    await application.onStart()
    activity = application.manager.findActiveActivity(TestActivity.action) as Activity
  })

  it('does not crash when there is no pending request code', async () => {
    assert.doesNotThrow(async () => {
      await activity.onActivityResult(2, IntentResult.RESULT_OK, { action: 'test' })
    })
  })

  it('removes request code if present', async () => {
    const requestCode = await activity.startActivityForResult({ action: TestActivity.action })
    assert.isTrue(activity.hasRequestCode(requestCode))
    await activity.onActivityResult(requestCode, IntentResult.RESULT_OK, { action: 'test' })
    assert.isFalse(activity.hasRequestCode(requestCode))
  })

  it('forwards to fragment if fragment has request code', async () => {
    const fragment = new TestFragment()
    const onActivityResultStub = sinon.spy(fragment, 'onActivityResult')
    sinon.stub(fragment, 'hasRequestCode').returns(true)
    sinon.stub(activity['manager'], 'findByRequestCode').returns(fragment as unknown as Fragment)
    const requestCode = 123
    await activity.onActivityResult(requestCode, IntentResult.RESULT_OK, { action: 'test' })
    assert.isTrue(onActivityResultStub.calledOnceWith(requestCode, IntentResult.RESULT_OK, { action: 'test' }))
  })

  it('logs info if not handled by fragment and is base Activity', async () => {
    sinon.stub(activity['manager'], 'findByRequestCode').returns(null)
    const consoleSpy = sinon.spy(console, 'info')
    const requestCode = 123
    // Ensure the activity instance is exactly Activity, not a subclass that might override onActivityResult
    Object.setPrototypeOf(activity, Activity.prototype)

    await activity.onActivityResult(requestCode, IntentResult.RESULT_OK, { action: 'test-intent' })
    assert.isTrue(
      consoleSpy.calledWith(
        `Activity#onActivityResult not implemented. Request code: ${requestCode}, result code: ${IntentResult.RESULT_OK}`,
        { action: 'test-intent' }
      )
    )
    consoleSpy.restore()
  })
})

describe('Activity Actions', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    // For these tests, we often need a fresh activity instance not necessarily from the manager's stack
    // to avoid side effects from application.onStart() unless specifically testing manager interactions.
    activity = new TestActivity(application)
  })

  it('startActivity calls application manager startActivity', async () => {
    const intent = { action: TestActivity2.action }
    const spy = sinon.spy(application.manager, 'startActivity')
    await application.onStart() // Ensure the manager is initialized
    await activity.startActivity(intent)
    assert.isTrue(spy.calledOnceWith(intent))
  })

  it('startActivityForResult calls application manager startActivityForResult and stores request code', async () => {
    const intent = { action: TestActivity2.action }
    const managerSpy = sinon.spy(application.manager, 'startActivityForResult')
    const createCodeSpy = sinon.stub(application.manager, 'createRequestCode').returns(999)
    await application.onStart() // Ensure the manager is initialized
    const requestCode = await activity.startActivityForResult(intent)

    assert.equal(requestCode, 999)
    assert.isTrue(managerSpy.calledOnceWith(intent, 999))
    assert.isTrue(activity.hasRequestCode(999))

    createCodeSpy.restore()
  })

  it('finish calls application manager finishActivity', async () => {
    const spy = sinon.spy(application.manager, 'finishActivity')
    await activity.finish()
    assert.isTrue(spy.calledOnceWith(activity))
  })

  it('requestUpdate calls parent application requestUpdate and manager updateActiveFragments', () => {
    const appSpy = sinon.spy(application, 'requestUpdate')
    const managerSpy = sinon.spy(activity['manager'], 'updateActiveFragments')
    const opts = { app: false, activity: true, fragment: false }
    activity.requestUpdate(opts)
    assert.isTrue(appSpy.calledOnceWith(opts))
    assert.isTrue(managerSpy.calledOnce)
  })

  it('requestUpdate defaults to updating activity fragments', () => {
    const managerSpy = sinon.spy(activity['manager'], 'updateActiveFragments')
    activity.requestUpdate() // No opts, so activity should be true by default
    assert.isTrue(managerSpy.calledOnce)
  })

  it('requestUpdate respects activity: false in opts', () => {
    const managerSpy = sinon.spy(activity['manager'], 'updateActiveFragments')
    activity.requestUpdate({ activity: false })
    assert.isFalse(managerSpy.called)
  })
})

describe('Fragment Management', () => {
  let application: Application
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    application = new TestApplication(renderRoot)
    activity = new TestActivity(application)
  })

  it('addFragment calls manager.attachFragment', async () => {
    const spy = sinon.spy(activity['manager'], 'attachFragment')
    const fragment = new TestFragment()
    const data = { info: 'test' }
    await activity.addFragment('testKey', fragment, data)
    assert.isTrue(spy.calledOnceWith('testKey', fragment, activity, data))
  })

  it('showFragment calls manager.showFragment', async () => {
    const spy = sinon.spy(activity['manager'], 'showFragment')
    activity.addFragment('testKey', new TestFragment()) // Ensure fragment exists
    const rootEl = document.createElement('div')
    await activity.showFragment('testKey', rootEl)
    assert.isTrue(spy.calledOnceWith('testKey', rootEl))
  })

  it('hideFragment calls manager.hideFragment if fragment exists', async () => {
    const fragment = new TestFragment()
    sinon.stub(activity['manager'], 'findFragment').returns(fragment)
    const spy = sinon.spy(activity['manager'], 'hideFragment')
    await activity.hideFragment('testKey')
    assert.isTrue(spy.calledOnceWith(fragment))
  })

  it('hideFragment does not call manager.hideFragment if fragment does not exist', async () => {
    sinon.stub(activity['manager'], 'findFragment').returns(undefined)
    const spy = sinon.spy(activity['manager'], 'hideFragment')
    await activity.hideFragment('testKey')
    assert.isFalse(spy.called)
  })
})

describe('handleIntentEvent', () => {
  let activity: Activity

  beforeEach(async () => {
    const renderRoot = await basicFixture()
    const application = new TestApplication(renderRoot)
    await application.onStart() // Ensure the manager is initialized
    activity = new TestActivity(application)
  })

  it('handles startActivityForResult event', async () => {
    const startActivityForResultSpy = sinon.spy(activity, 'startActivityForResult')
    const detail = { intent: { action: TestActivity2.action }, onResult: () => {} }
    const event = new CustomEvent(EventTypes.Intent.startActivityForResult, { detail })
    await activity.handleIntentEvent(event)
    assert.isTrue(startActivityForResultSpy.calledOnceWith(detail.intent))
  })

  it('handles startActivity event', async () => {
    const startActivitySpy = sinon.spy(activity, 'startActivity')
    const detail = { intent: { action: TestActivity2.action } }
    const event = new CustomEvent(EventTypes.Intent.startActivity, { detail })
    await activity.handleIntentEvent(event)
    assert.isTrue(startActivitySpy.calledOnceWith(detail.intent))
  })

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