import { describe, expect, it, vi } from 'vitest';

import { TeamTaskStallSnapshotSource } from '../../../../../src/main/services/team/stallMonitor/TeamTaskStallSnapshotSource';

describe('TeamTaskStallSnapshotSource', () => {
  it('returns null when transcript context is unavailable', async () => {
    const source = new TeamTaskStallSnapshotSource(
      { getContext: vi.fn(async () => null) } as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never
    );

    await expect(source.getSnapshot('demo')).resolves.toBeNull();
  });

  it('builds one batched snapshot and narrows exact/freshness reads to work and started-review candidates', async () => {
    const activeTasks = [
      { id: 'task-a', subject: 'A', status: 'in_progress' },
      {
        id: 'task-b',
        subject: 'B',
        status: 'completed',
        reviewState: 'review',
        historyEvents: [
          {
            id: 'evt-review-requested',
            type: 'review_requested',
            timestamp: '2026-04-19T12:00:00.000Z',
            from: 'none',
            to: 'review',
            reviewer: 'alice',
          },
        ],
      },
      { id: 'task-approved', subject: 'Approved', status: 'in_progress' },
      { id: 'task-reopened', subject: 'Reopened', status: 'pending', reviewState: 'approved' },
    ];
    const deletedTasks = [{ id: 'task-deleted', subject: 'D', status: 'deleted' }];
    const transcriptContext = {
      projectDir: '/tmp/project',
      projectId: 'project-id',
      config: {
        members: [
          { name: 'team-lead', role: 'team lead', providerId: 'codex' },
          { name: 'alice', role: 'Developer', model: 'qwen/qwen3-coder' },
        ],
      } as never,
      sessionIds: ['session-a'],
      transcriptFiles: ['/tmp/project/session-a.jsonl', '/tmp/project/session-b.jsonl'],
    };
    const rawMessages = [{ uuid: 'm1' }];
    const recordsByTaskId = new Map([
      [
        'task-a',
        [
          {
            id: 'r1',
            source: {
              filePath: '/tmp/project/session-b.jsonl',
            },
          },
        ],
      ],
      [
        'task-b',
        [
          {
            id: 'r2',
            source: {
              filePath: '/tmp/project/session-a.jsonl',
            },
          },
        ],
      ],
    ]);
    const freshnessByTaskId = new Map([
      ['task-a', { taskId: 'task-a', updatedAt: '2026-04-19T12:00:00.000Z', filePath: '/tmp/fresh.json' }],
    ]);
    const exactRowsByFilePath = new Map([['/tmp/project/session-b.jsonl', []]]);

    const locator = {
      getContext: vi.fn(async () => transcriptContext),
    };
    const taskReader = {
      getTasks: vi.fn(async () => activeTasks),
      getDeletedTasks: vi.fn(async () => deletedTasks),
    };
    const kanbanManager = {
      getState: vi.fn(async () => ({
        teamName: 'demo',
        reviewers: ['alice'],
        tasks: {
          'task-b': {
            column: 'review',
            movedAt: '2026-04-19T12:00:00.000Z',
            reviewer: 'alice',
          },
          'task-approved': {
            column: 'approved',
            movedAt: '2026-04-19T12:05:00.000Z',
          },
        },
      })),
    };
    const transcriptReader = {
      readFiles: vi.fn(async () => rawMessages),
    };
    const batchIndexer = {
      buildIndex: vi.fn(() => recordsByTaskId),
    };
    const freshnessReader = {
      readSignals: vi.fn(async () => freshnessByTaskId),
    };
    const exactRowReader = {
      parseFiles: vi.fn(async () => exactRowsByFilePath),
    };
    const membersMetaStore = {
      getMembers: vi.fn(async () => [{ name: 'alice', providerId: 'opencode' }]),
    };
    const openCodeEvidenceSource = {
      readEvidence: vi.fn(async () => ({
        recordsByTaskId: new Map(),
        exactRowsByFilePath: new Map(),
      })),
    };

    const source = new TeamTaskStallSnapshotSource(
      locator as never,
      taskReader as never,
      kanbanManager as never,
      transcriptReader as never,
      batchIndexer as never,
      freshnessReader as never,
      exactRowReader as never,
      membersMetaStore as never,
      openCodeEvidenceSource as never
    );

    const snapshot = await source.getSnapshot('demo');
    const expectedWorkflowActiveTasks = [
      activeTasks[0],
      activeTasks[1],
      { ...activeTasks[2], reviewState: 'approved' },
      { ...activeTasks[3], reviewState: 'none' },
    ];

    expect(snapshot).not.toBeNull();
    expect(batchIndexer.buildIndex).toHaveBeenCalledWith({
      teamName: 'demo',
      tasks: [...expectedWorkflowActiveTasks, ...deletedTasks],
      messages: rawMessages,
    });
    expect(freshnessReader.readSignals).toHaveBeenCalledWith('/tmp/project', ['task-a', 'task-b'], {
      teamName: 'demo',
    });
    expect(exactRowReader.parseFiles).toHaveBeenCalledWith(['/tmp/project/session-a.jsonl', '/tmp/project/session-b.jsonl']);
    expect(openCodeEvidenceSource.readEvidence).toHaveBeenCalledWith({
      teamName: 'demo',
      tasks: [expectedWorkflowActiveTasks[0], expectedWorkflowActiveTasks[1]],
      providerByMemberName: new Map([
        ['team-lead', 'codex'],
        ['alice', 'opencode'],
      ]),
    });
    expect(snapshot?.activeTasks).toEqual(expectedWorkflowActiveTasks);
    expect(snapshot?.inProgressTasks.map((task) => task.id)).toEqual(['task-a']);
    expect(snapshot?.reviewOpenTasks.map((task) => task.id)).toEqual(['task-b']);
    expect(snapshot?.leadName).toBe('team-lead');
    expect(snapshot?.providerByMemberName).toEqual(
      new Map([
        ['team-lead', 'codex'],
        ['alice', 'opencode'],
      ])
    );
    expect(snapshot?.resolvedReviewersByTaskId.get('task-b')).toEqual({
      reviewer: 'alice',
      source: 'kanban_state',
    });
    expect(snapshot?.recordsByTaskId).toBe(recordsByTaskId);
  });

  it('merges OpenCode runtime evidence even when no Claude transcript files are available', async () => {
    const task = {
      id: 'task-open',
      displayId: 'opencode1',
      subject: 'OpenCode task',
      status: 'in_progress',
      owner: 'bob',
    };
    const openCodeRecord = {
      id: 'opencode-rec',
      timestamp: '2026-04-19T12:00:00.000Z',
      source: {
        filePath: 'opencode-runtime:demo:bob',
        sourceOrder: 1,
      },
    };
    const openCodeRows = [
      {
        filePath: 'opencode-runtime:demo:bob',
        sourceOrder: 1,
        messageUuid: 'msg-open',
        timestamp: '2026-04-19T12:00:00.000Z',
        parsedMessage: {
          uuid: 'msg-open',
          parentUuid: null,
          type: 'assistant',
          timestamp: new Date('2026-04-19T12:00:00.000Z'),
          content: '',
          isSidechain: true,
          isMeta: false,
          toolCalls: [],
          toolResults: [],
        },
        toolUseIds: [],
        toolResultIds: [],
      },
    ];
    const source = new TeamTaskStallSnapshotSource(
      {
        getContext: vi.fn(async () => ({
          projectDir: '/tmp/project',
          projectId: 'project-id',
          config: {
            members: [
              { name: 'team-lead', role: 'team lead', providerId: 'codex' },
              { name: 'bob', role: 'Developer', providerId: 'opencode' },
            ],
          },
          sessionIds: [],
          transcriptFiles: [],
        })),
      } as never,
      {
        getTasks: vi.fn(async () => [task]),
        getDeletedTasks: vi.fn(async () => []),
      } as never,
      {
        getState: vi.fn(async () => ({ teamName: 'demo', tasks: {} })),
      } as never,
      {
        readFiles: vi.fn(async () => {
          throw new Error('transcript reader should not be called');
        }),
      } as never,
      {
        buildIndex: vi.fn(() => new Map()),
      } as never,
      {
        readSignals: vi.fn(async () => new Map()),
      } as never,
      {
        parseFiles: vi.fn(async () => new Map()),
      } as never,
      {
        getMembers: vi.fn(async () => []),
      } as never,
      {
        readEvidence: vi.fn(async () => ({
          recordsByTaskId: new Map([['task-open', [openCodeRecord]]]),
          exactRowsByFilePath: new Map([['opencode-runtime:demo:bob', openCodeRows]]),
        })),
      } as never
    );

    const snapshot = await source.getSnapshot('demo');

    expect(snapshot?.recordsByTaskId.get('task-open')).toEqual([openCodeRecord]);
    expect(snapshot?.exactRowsByFilePath.get('opencode-runtime:demo:bob')).toEqual(openCodeRows);
    expect(snapshot?.transcriptFiles).toEqual([]);
  });
});
