import '../../../_node-compat.js';

import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixRuntime } from '../../../runtime/MatrixRuntime.js';
import { MatrixActor } from '../../MatrixActor.js';

class DisposeProbeActor extends MatrixActor {
  static accepts = {
    'probe.ping': { description: 'No-op probe' },
  };

  override onDispose(): void {
    this.emit('probe.disposing', { mount: this.mount });
  }

  emitAfterDisposeForTest(): void {
    this.emit('probe.after-dispose', { mount: this.mount });
  }

  emitDuringClosedTransportForTest(): void {
    this.emit('probe.closed-transport', { mount: this.mount });
  }
}

class ShutdownProbeActor extends MatrixActor {
  static accepts = {
    'probe.shutdown': { description: 'No-op probe' },
  };

  override onDispose(): void {
    setTimeout(() => {
      this.emit('probe.shutdown-late', { mount: this.mount });
    }, 0);
  }
}

describe('MatrixActor dispose lifecycle', () => {
  let runtime: MatrixRuntime | null = null;

  afterEach(async () => {
    if (runtime) {
      await runtime.shutdown();
      runtime = null;
    }
  });

  it('suppresses disposed-actor warnings during onDispose but still warns after disposal', async () => {
    runtime = new MatrixRuntime({ logging: false });
    const warnCalls: string[] = [];
    const originalWarn = console.warn;
    console.warn = (...args: unknown[]) => {
      warnCalls.push(args.map((arg) => String(arg)).join(' '));
    };

    try {
      const actor = await runtime.create(DisposeProbeActor, 'probe.dispose', {});
      await actor.ready;

      await runtime.remove('probe.dispose');
      assert.equal(
        warnCalls.filter((entry) => entry.includes('Cannot emit from disposed actor')).length,
        0,
        'dispose cascade should not warn while the actor is disposing itself',
      );

      actor.emitAfterDisposeForTest();
      assert.equal(
        warnCalls.filter((entry) => entry.includes('Cannot emit from disposed actor: probe.dispose')).length,
        1,
        'post-disposal emits should still warn',
      );
    } finally {
      console.warn = originalWarn;
    }
  });

  it('suppresses late disposed-actor warnings while the runtime is shutting down', async () => {
    runtime = new MatrixRuntime({ logging: false });
    const warnCalls: string[] = [];
    const originalWarn = console.warn;
    console.warn = (...args: unknown[]) => {
      warnCalls.push(args.map((arg) => String(arg)).join(' '));
    };

    try {
      const actor = await runtime.create(ShutdownProbeActor, 'probe.shutdown', {});
      await actor.ready;

      await runtime.shutdown();
      runtime = null;
      await new Promise((resolve) => setTimeout(resolve, 20));

      assert.equal(
        warnCalls.filter((entry) => entry.includes('Cannot emit from disposed actor')).length,
        0,
        'runtime shutdown should suppress disposal-noise warnings from late emits',
      );
    } finally {
      console.warn = originalWarn;
    }
  });

  it('suppresses closed-transport publish errors for late fire-and-forget emits', async () => {
    runtime = new MatrixRuntime({ logging: false });
    const actor = await runtime.create(DisposeProbeActor, 'probe.transport-close', {});
    await actor.ready;

    const transport = (actor as any)._context.transport as { publish: (topic: string, payload?: unknown) => void };
    const originalPublish = transport.publish.bind(transport);
    transport.publish = () => {
      const error = new Error('CONNECTION_CLOSED');
      (error as Error & { code?: string }).code = 'CONNECTION_CLOSED';
      throw error;
    };

    try {
      assert.doesNotThrow(
        () => actor.emitDuringClosedTransportForTest(),
        'late status/event emits should not throw when the transport has already closed',
      );
    } finally {
      transport.publish = originalPublish;
    }
  });
});
