/**
 * Behavioral tests for PackageRootActor storage seam (HARVEST-001 / HARVEST-002).
 *
 * Two layers:
 *
 *   1. Fast unit tests of _initStorage against a minimal fake context.
 *      No MatrixRuntime, no transport, no $join, no shutdown wait.
 *      Proves the contract:
 *        - provider present + returns store -> RECORD_STORE_KEY set
 *        - no provider -> deferred (no RECORD_STORE_KEY, no throw)
 *        - provider returns null -> deferred
 *        - provider throws -> deferred (bootstrap unaffected)
 *        - manifest declares no storage -> provider not invoked
 *        - provider invoked with (matrixHome, domain) where domain comes from manifest
 *      Each test runs in <10ms.
 *
 *   2. Runtime-wiring test: a single test that proves MatrixRuntime registers
 *      the provider on its rootContext under RECORD_STORE_PROVIDER_KEY when
 *      `recordStoreProvider` is supplied via IRuntimeConfig. Uses runtime.shutdown()
 *      to exit cleanly. This is the only test that touches a real runtime.
 *
 * Why this split:
 *   - The seam is a context-service contract. The unit tests test the contract.
 *   - The runtime-wiring test proves IRuntimeConfig.recordStoreProvider lands
 *     where PackageRootActor will look for it, without dragging the actor
 *     bootstrap path (and its $join wait) into the assertion loop.
 */

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

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixRuntime } from '../../runtime/MatrixRuntime.js';
import { PackageRootActor, type PackageManifestProps } from '../PackageRootActor.js';
import { RECORD_STORE_PROVIDER_KEY, type IRecordStoreProvider } from '../IRecordStoreProvider.js';
import { RECORD_STORE_KEY } from '../../framework/InteractionRecord.js';

// ── Fake context (cascading service registry, no runtime) ───────────────────

interface IServiceLookup {
  getService(key: string | symbol): unknown;
  setService(key: string | symbol, value: unknown): void;
}

class FakeContext implements IServiceLookup {
  private readonly services = new Map<string | symbol, unknown>();
  constructor(private readonly parent?: IServiceLookup) {}
  getService(key: string | symbol): unknown {
    if (this.services.has(key)) return this.services.get(key);
    return this.parent?.getService(key);
  }
  setService(key: string | symbol, value: unknown): void {
    this.services.set(key, value);
  }
}

// ── Test PackageRootActor that bypasses MatrixActor mounting ────────────────

class TestPackageRoot extends PackageRootActor {
  static override package = '@open-matrix/test-package';
  static override namespace = 'test.pkg';

  /**
   * Set both _props (what onBootstrap reads) and _manifestProps (what
   * _initStorage reads). onBootstrap normally copies _props -> _manifestProps;
   * the test bypasses bootstrap so we copy here.
   */
  setManifestProps(props: PackageManifestProps): void {
    (this as unknown as { _props: PackageManifestProps })._props = props;
    (this as unknown as { _manifestProps: PackageManifestProps })._manifestProps = props;
  }
  attachContext(ctx: IServiceLookup): void {
    (this as unknown as { _context: IServiceLookup })._context = ctx;
  }
  async runInitStorage(): Promise<void> {
    await (this as unknown as { _initStorage(): Promise<void> })._initStorage();
  }
}

function makeContextWithProvider(provider: IRecordStoreProvider): FakeContext {
  const root = new FakeContext();
  root.setService(RECORD_STORE_PROVIDER_KEY, provider);
  return new FakeContext(root); // child cascades to root
}

function makeContextNoProvider(): FakeContext {
  const root = new FakeContext();
  return new FakeContext(root);
}

// ── Unit tests (fast: no runtime) ────────────────────────────────────────────

describe('PackageRootActor storage seam — unit', () => {

  it('sets RECORD_STORE_KEY when provider returns a store; passes (matrixHome, domain)', async () => {
    const fakeStore = { __probe: 'fake-store-handle' };
    const calls: Array<{ matrixHome: string; domain: string }> = [];
    const provider: IRecordStoreProvider = {
      async provide(matrixHome, domain) {
        calls.push({ matrixHome, domain });
        return fakeStore;
      },
    };
    const ctx = makeContextWithProvider(provider);

    const actor = new TestPackageRoot();
    actor.setManifestProps({
      storage: { sqlite: { domain: 'test-domain' } },
      packageName: '@open-matrix/test-package',
    });
    actor.attachContext(ctx);

    await actor.runInitStorage();

    assert.equal(calls.length, 1, 'provider invoked exactly once');
    assert.equal(calls[0]!.domain, 'test-domain', 'domain matches manifest');
    assert.ok(calls[0]!.matrixHome.length > 0, 'matrixHome resolved (non-empty)');
    assert.deepEqual(ctx.getService(RECORD_STORE_KEY), fakeStore, 'store registered on context');
  });

  it('does not set RECORD_STORE_KEY when no provider is registered (deferred)', async () => {
    const ctx = makeContextNoProvider();

    const actor = new TestPackageRoot();
    actor.setManifestProps({
      storage: { sqlite: { domain: 'test-domain-2' } },
      packageName: '@open-matrix/test-package',
    });
    actor.attachContext(ctx);

    await actor.runInitStorage();

    assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'no store; deferred');
  });

  it('does not set RECORD_STORE_KEY when provider returns null (declined)', async () => {
    const provider: IRecordStoreProvider = { async provide() { return null; } };
    const ctx = makeContextWithProvider(provider);

    const actor = new TestPackageRoot();
    actor.setManifestProps({
      storage: { sqlite: { domain: 'test-domain-3' } },
      packageName: '@open-matrix/test-package',
    });
    actor.attachContext(ctx);

    await actor.runInitStorage();

    assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'declined provider; deferred');
  });

  it('swallows provider errors; bootstrap not affected (matches pre-seam swallow)', async () => {
    const provider: IRecordStoreProvider = {
      async provide() { throw new Error('synthetic provider failure'); },
    };
    const ctx = makeContextWithProvider(provider);

    const actor = new TestPackageRoot();
    actor.setManifestProps({
      storage: { sqlite: { domain: 'test-domain-4' } },
      packageName: '@open-matrix/test-package',
    });
    actor.attachContext(ctx);

    await actor.runInitStorage(); // must not throw

    assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'failed provider; deferred');
  });

  it('does not invoke provider when manifest has no storage declaration', async () => {
    let providerCalled = false;
    const provider: IRecordStoreProvider = {
      async provide() { providerCalled = true; return { dummy: true }; },
    };
    const ctx = makeContextWithProvider(provider);

    const actor = new TestPackageRoot();
    actor.setManifestProps({
      packageName: '@open-matrix/test-package',
    });
    actor.attachContext(ctx);

    await actor.runInitStorage();

    assert.equal(providerCalled, false, 'no storage declared -> provider not invoked');
    assert.equal(ctx.getService(RECORD_STORE_KEY), undefined);
  });

  it('passes the exact domain string from manifest (per-package data isolation)', async () => {
    // Defensive: HARVEST-SQLITE-ARCH says each package/domain owns its own
    // <matrixHome>/data/<domain>/store.db — provider must receive the domain
    // verbatim so two packages with different domains never collide.
    const observedDomains: string[] = [];
    const provider: IRecordStoreProvider = {
      async provide(_matrixHome, domain) {
        observedDomains.push(domain);
        return { handle: domain };
      },
    };
    const root = new FakeContext();
    root.setService(RECORD_STORE_PROVIDER_KEY, provider);

    for (const domain of ['pkg-a', 'pkg-b', 'system.observability']) {
      const ctx = new FakeContext(root);
      const actor = new TestPackageRoot();
      actor.setManifestProps({ storage: { sqlite: { domain } }, packageName: domain });
      actor.attachContext(ctx);
      await actor.runInitStorage();
    }

    assert.deepEqual(observedDomains, ['pkg-a', 'pkg-b', 'system.observability']);
  });

  it('seam key matches the symbol the runtime config registers under', () => {
    // Symbol.for is global so this is identity-equal across modules; the
    // assertion guards against accidental divergence in future edits.
    assert.equal(
      RECORD_STORE_PROVIDER_KEY,
      Symbol.for('matrix.record-store.provider'),
    );
  });

});

// ── Runtime wiring (single integration assertion) ────────────────────────────

describe('MatrixRuntime registers IRecordStoreProvider on rootContext', () => {

  it('places config.recordStoreProvider at RECORD_STORE_PROVIDER_KEY on rootContext', async () => {
    const provider: IRecordStoreProvider = { async provide() { return null; } };

    const runtime = new MatrixRuntime({
      logging: false,
      recordStoreProvider: provider,
    });

    try {
      const ctx = (runtime as unknown as { rootContext: { getService(k: symbol): unknown } }).rootContext;
      const found = ctx.getService(RECORD_STORE_PROVIDER_KEY);
      assert.equal(found, provider, 'runtime registered provider under expected key');
    } finally {
      await runtime.shutdown();
    }
  });

  it('does not register a provider when config.recordStoreProvider is omitted', async () => {
    const runtime = new MatrixRuntime({ logging: false });

    try {
      const ctx = (runtime as unknown as { rootContext: { getService(k: symbol): unknown } }).rootContext;
      const found = ctx.getService(RECORD_STORE_PROVIDER_KEY);
      assert.equal(found, undefined, 'no provider registered; consumers see undefined');
    } finally {
      await runtime.shutdown();
    }
  });

});
