import { beforeEach, describe, expect, it } from 'vitest';
import { NotepadService } from '../../src/notepads/service.js';
import { executeNotepadTool, notepadToolDescription } from '../../src/notepads/tool.js';
import { MemoryStore } from '../../src/store/memory.js';
import type { ExplicitNotepadRecord, SessionRecord } from '../../src/store/types.js';

const now = new Date('2026-07-21T00:00:00Z');
const system = { bypass: true, user: null } as const;

describe('Pi Notepad tool', () => {
  let store: MemoryStore;
  let service: NotepadService;
  let own: SessionRecord;
  const run = (params: unknown) =>
    executeNotepadTool({ store, notepads: service, sessionId: own.id, runId: 'run-7', messageId: 'message-8' }, params);
  beforeEach(async () => {
    store = new MemoryStore();
    service = new NotepadService(store);
    await store.upsertAuthUserForAccount({
      userId: 'owner',
      accountId: 'owner-account',
      provider: 'test',
      providerAccountId: 'owner',
      username: 'owner',
      role: 'member',
      profile: {},
      now,
    });
    own = await store.createSession(session('own', 'owner'));
  });

  it('lazily reads/mutates its own Session Notepad with run attribution', async () => {
    await expect(run({ action: 'read' })).resolves.toMatchObject({ ok: true, result: { revision: 0, content: '' } });
    expect(await store.getSessionNotepad(own.id)).toBeNull();
    await expect(run({ action: 'replace', content: 'memory', expectedRevision: 0 })).resolves.toMatchObject({
      ok: true,
      result: { revision: 1 },
    });
    expect((await store.listNotepadRevisions('session', own.id, 50, 0)).items[0]).toMatchObject({
      actor: { kind: 'agent', sessionId: own.id, runId: 'run-7' },
    });
  });

  it('allows a private session agent to use its own and associated Explicit Notepads', async () => {
    own = await store.createSession({
      ...session('private-own', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    await expect(run({ action: 'replace', content: 'private memory', expectedRevision: 0 })).resolves.toMatchObject({
      ok: true,
      result: { revision: 1 },
    });
    await expect(run({ action: 'history' })).resolves.toMatchObject({
      ok: true,
      result: { revisions: [{ revision: 1 }] },
    });

    const created = await run({ action: 'create', title: 'Private agent pad', content: 'start' });
    expect(created).toMatchObject({ ok: true });
    const notepadId = (created.result as ExplicitNotepadRecord).id;
    await expect(run({ action: 'append', notepadId, append: ' end' })).resolves.toMatchObject({
      ok: true,
      result: { revision: 2 },
    });
    await expect(run({ action: 'read_revision', notepadId, revision: 1 })).resolves.toMatchObject({
      ok: true,
      result: { content: 'start' },
    });
  });

  it('supports own history/read/restore and explicit self-target actions without capabilities', async () => {
    await run({ action: 'replace', content: 'first', expectedRevision: 0 });
    await run({ action: 'replace', content: 'second', expectedRevision: 1 });
    await expect(run({ action: 'history' })).resolves.toMatchObject({
      result: { revisions: [{ revision: 2 }, { revision: 1 }] },
    });
    await expect(run({ action: 'read_revision', revision: 1 })).resolves.toMatchObject({
      result: { content: 'first' },
    });
    await expect(run({ action: 'restore_revision', revision: 1, expectedRevision: 2 })).resolves.toMatchObject({
      result: { revision: 3 },
    });
    await expect(run({ action: 'append_session', sessionId: own.id, append: '!' })).resolves.toMatchObject({
      ok: true,
      result: { revision: 4 },
    });
  });

  it('creates explicit content atomically, associates it, and allows associated writes', async () => {
    const created = await run({ action: 'create', title: 'Plan', content: '# Plan' });
    const id = (created.result as ExplicitNotepadRecord).id;
    expect(created).toMatchObject({ ok: true, result: { revision: 1, sizeBytes: 6 } });
    await expect(run({ action: 'list' })).resolves.toMatchObject({ result: { notepads: [{ notepadId: id }] } });
    await expect(run({ action: 'append', notepadId: id, append: '!' })).resolves.toMatchObject({
      result: { revision: 2 },
    });
    await expect(store.getNotepadRevision('explicit', id, 1)).resolves.toMatchObject({
      content: '# Plan',
      actor: { kind: 'agent' },
    });
  });

  it('denies dormant Explicit Notepad access and grants another Session', async () => {
    const pad = await service.create(system, { title: 'pad' });
    await expect(run({ action: 'read', notepadId: pad.id })).resolves.toMatchObject({
      ok: false,
      error: expect.stringContaining('denied'),
    });
    await service.putAssociation(system, pad.id, own.id, { kind: 'system' });
    const peer = await store.createSession({ ...session('peer', 'owner'), parentSessionId: own.id, spawnDepth: 1 });
    await expect(run({ action: 'grant', notepadId: pad.id, sessionId: peer.id })).resolves.toMatchObject({
      ok: true,
      result: { sessionId: peer.id },
    });
  });

  it('searches and reads Explicit Notepads through readable Session associations but keeps writes associated', async () => {
    const pad = await service.create(system, { title: 'searchable needle' });
    const peer = await store.createSession(session('search-peer', 'owner'));
    await service.putAssociation(system, pad.id, peer.id, { kind: 'system' });
    await expect(run({ action: 'search', query: 'needle' })).resolves.toMatchObject({
      ok: true,
      result: { results: [{ id: pad.id }] },
    });
    await expect(run({ action: 'read', notepadId: pad.id })).resolves.toMatchObject({ ok: true });
    await expect(run({ action: 'append', notepadId: pad.id, append: 'no' })).resolves.toMatchObject({ ok: false });
  });

  it('directly reads archived Explicit Notepads and archived-only associations without searching them', async () => {
    const pad = await service.createForSessionAgent(
      own.id,
      { title: 'archived-search-needle', content: 'durable' },
      { kind: 'agent', sessionId: own.id, runId: 'archive-run' },
    );
    await store.archiveExplicitNotepad({ id: pad.id, archivedAt: now });
    await expect(run({ action: 'search', query: 'archived-search-needle' })).resolves.toMatchObject({
      result: { results: [] },
    });
    await expect(run({ action: 'read', notepadId: pad.id })).resolves.toMatchObject({
      ok: true,
      result: { content: 'durable' },
    });

    await store.restoreExplicitNotepad({ id: pad.id, updatedAt: now });
    await store.archiveSession({ sessionId: own.id, archivedAt: now });
    await expect(run({ action: 'search', query: 'archived-search-needle' })).resolves.toMatchObject({
      result: { results: [] },
    });
    await expect(run({ action: 'read', notepadId: pad.id })).resolves.toMatchObject({
      ok: true,
      result: { content: 'durable' },
    });
  });

  it('keeps Explicit Notepads associated only with private Sessions owner-isolated', async () => {
    await store.upsertAuthUserForAccount({
      userId: 'other-owner',
      accountId: 'other-owner-account',
      provider: 'test',
      providerAccountId: 'other-owner',
      username: 'other-owner',
      role: 'member',
      profile: {},
      now,
    });
    const privateActor = await store.createSession({
      ...session('private-search-actor', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    const sameOwnerTarget = await store.createSession({
      ...session('private-search-target', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    const otherOwnerTarget = await store.createSession({
      ...session('other-private-search-target', 'other-owner'),
      visibility: 'private',
      ownerUserId: 'other-owner',
    });
    const sameOwnerPad = await service.create(system, { title: 'same-owner-secret' });
    const otherOwnerPad = await service.create(system, { title: 'other-owner-secret' });
    await store.putNotepadAssociation({
      record: { notepadId: sameOwnerPad.id, sessionId: sameOwnerTarget.id, createdAt: now },
      actor: { kind: 'system' },
      activityId: 'same-owner-association',
    });
    await store.putNotepadAssociation({
      record: { notepadId: otherOwnerPad.id, sessionId: otherOwnerTarget.id, createdAt: now },
      actor: { kind: 'system' },
      activityId: 'other-owner-association',
    });

    await expect(run({ action: 'search', query: 'owner-secret' })).resolves.toMatchObject({
      result: { results: [] },
    });
    await expect(run({ action: 'read', notepadId: sameOwnerPad.id })).resolves.toMatchObject({ ok: false });

    own = privateActor;
    await expect(run({ action: 'search', query: 'owner-secret' })).resolves.toMatchObject({
      result: { results: [{ id: sameOwnerPad.id }] },
    });
    await expect(run({ action: 'read', notepadId: sameOwnerPad.id })).resolves.toMatchObject({ ok: true });
    await expect(run({ action: 'read', notepadId: otherOwnerPad.id })).resolves.toMatchObject({ ok: false });
  });

  it('coordinates available Session Notepads without a separate capability grant', async () => {
    const peer = await store.createSession(session('peer', 'someone'));
    await expect(run({ action: 'append_session', sessionId: peer.id, append: 'handoff' })).resolves.toMatchObject({
      ok: true,
    });
    await store.updateAuthUserRole({ userId: 'owner', role: 'viewer', updatedAt: now });
    await expect(run({ action: 'read_session', sessionId: peer.id })).resolves.toMatchObject({
      ok: true,
    });
    await expect(
      run({ action: 'append_session', sessionId: peer.id, append: ' still available' }),
    ).resolves.toMatchObject({
      ok: true,
    });
  });

  it('allows same-owner private Session Notepads but blocks tenant or promoted agents', async () => {
    const target = await store.createSession({
      ...session('private-target', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    const privateActor = await store.createSession({
      ...session('private-actor', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    await expect(run({ action: 'read_session', sessionId: target.id })).resolves.toMatchObject({ ok: false });
    own = privateActor;
    await expect(
      run({ action: 'append_session', sessionId: target.id, append: 'allowed while private' }),
    ).resolves.toMatchObject({ ok: true });

    await store.updateSessionMetadataWithEvent({ id: privateActor.id, promoteToTenant: true, updatedAt: now });
    await expect(
      run({ action: 'append_session', sessionId: target.id, append: 'blocked after promotion' }),
    ).resolves.toMatchObject({ ok: false });
  });

  it('reads archived Session Notepads but does not update them', async () => {
    const peer = await store.createSession(session('archived-peer', 'owner'));
    await service.mutateSession(system, peer.id, { content: 'finished', expectedRevision: 0 }, { kind: 'system' });
    await store.archiveSession({ sessionId: peer.id, archivedAt: now });

    await expect(run({ action: 'read_session', sessionId: peer.id })).resolves.toMatchObject({
      ok: true,
      result: { content: 'finished' },
    });
    await expect(run({ action: 'append_session', sessionId: peer.id, append: 'no' })).resolves.toMatchObject({
      ok: false,
    });

    await store.unarchiveSession({ sessionId: peer.id, unarchivedAt: now });
    await store.archiveSession({ sessionId: own.id, archivedAt: now });
    await expect(run({ action: 'read_session', sessionId: peer.id })).resolves.toMatchObject({
      ok: true,
      result: { content: 'finished' },
    });
  });

  it('grants Explicit Notepad associations only to sessions readable by the acting private agent', async () => {
    await store.upsertAuthUserForAccount({
      userId: 'other-owner',
      accountId: 'other-owner-account',
      provider: 'test',
      providerAccountId: 'other-owner',
      username: 'other-owner',
      role: 'member',
      profile: {},
      now,
    });
    const privateActor = await store.createSession({
      ...session('private-association-actor', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    const sameOwnerTarget = await store.createSession({
      ...session('same-owner-target', 'owner'),
      visibility: 'private',
      ownerUserId: 'owner',
    });
    const otherOwnerTarget = await store.createSession({
      ...session('other-owner-target', 'other-owner'),
      visibility: 'private',
      ownerUserId: 'other-owner',
    });
    const pad = await service.create(system, { title: 'Private coordination' });
    const owner = await store.getAuthUser('owner');
    if (!owner) throw new Error('Expected owner fixture');
    await service.putAssociation({ bypass: false, user: owner }, pad.id, privateActor.id, { kind: 'system' });
    own = privateActor;

    await expect(run({ action: 'grant', notepadId: pad.id, sessionId: sameOwnerTarget.id })).resolves.toMatchObject({
      ok: true,
    });
    await expect(run({ action: 'revoke', notepadId: pad.id, sessionId: sameOwnerTarget.id })).resolves.toMatchObject({
      ok: true,
      result: { removed: true },
    });
    await expect(run({ action: 'grant', notepadId: pad.id, sessionId: otherOwnerTarget.id })).resolves.toMatchObject({
      ok: false,
    });

    await store.updateSessionMetadataWithEvent({ id: privateActor.id, promoteToTenant: true, updatedAt: now });
    await expect(run({ action: 'grant', notepadId: pad.id, sessionId: sameOwnerTarget.id })).resolves.toMatchObject({
      ok: false,
    });
  });

  it('rejects archived actors/targets, invalid actions, and bounds multibyte reads/history', async () => {
    const pad = await service.createForSessionAgent(
      own.id,
      { title: 'large' },
      { kind: 'agent', sessionId: own.id, runId: 'r' },
    );
    await run({ action: 'replace', notepadId: pad.id, content: '💥'.repeat(9_000), expectedRevision: 0 });
    const read = await run({ action: 'read', notepadId: pad.id });
    expect(Buffer.byteLength((read.result as { content: string }).content)).toBeLessThanOrEqual(32 * 1024);
    for (let i = 0; i < 55; i++) await run({ action: 'append', notepadId: pad.id, append: 'x' });
    expect(
      ((await run({ action: 'history', notepadId: pad.id })).result as { revisions: unknown[] }).revisions,
    ).toHaveLength(50);
    await store.archiveSession({ sessionId: own.id, archivedAt: now });
    await expect(run({ action: 'append', append: 'no' })).resolves.toMatchObject({
      ok: false,
      error: expect.stringContaining('Archived'),
    });
    await expect(run({ action: 'bogus' })).resolves.toMatchObject({ ok: false, error: 'Invalid notepad action' });
  });

  it('guidance promises durable memory without chain-of-thought or wake semantics', () => {
    expect(notepadToolDescription).toMatch(/Durable external memory/i);
    expect(notepadToolDescription).toMatch(/do not .*wake/i);
    expect(notepadToolDescription).not.toMatch(/chain[- ]of[- ]thought|reasoning trace/i);
  });
});

function session(id: string, createdByUserId: string): SessionRecord {
  return {
    id,
    createdByUserId,
    status: 'idle',
    spawnDepth: 0,
    tags: [],
    createdAt: now,
    updatedAt: now,
    lastActivityAt: now,
  };
}
