/* eslint-disable security/detect-non-literal-fs-filename -- Fixture E2E reads a repo fixture and writes temp JSONL. */
import { readFile, rm, stat, writeFile, mkdtemp } from 'fs/promises';
import os from 'os';
import path from 'path';
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { GetMemberLogStreamUseCase } from '../../../../../src/features/member-log-stream/core/application/use-cases/GetMemberLogStreamUseCase';
import {
  type MemberLogStreamRequestOptions,
  type MemberLogStreamResponse,
  type MemberRuntimeLogTailOptions,
  type MemberRuntimeLogTailResponse,
} from '../../../../../src/features/member-log-stream/contracts';
import { ClaudeMemberTranscriptStreamSource } from '../../../../../src/features/member-log-stream/main/adapters/output/sources/ClaudeMemberTranscriptStreamSource';
import { OpenCodeMemberRuntimeStreamSource } from '../../../../../src/features/member-log-stream/main/adapters/output/sources/OpenCodeMemberRuntimeStreamSource';
import { BoardTaskExactLogChunkBuilder } from '../../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder';
import { BoardTaskExactLogStrictParser } from '../../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogStrictParser';
import { TooltipProvider } from '../../../../../src/renderer/components/ui/tooltip';

import type { OpenCodeRuntimeTranscriptResponse } from '../../../../../src/main/services/runtime/ClaudeMultimodelBridgeService';
import type { MemberLogFileRef } from '../../../../../src/main/services/team/TeamMemberLogsFinder';
import type { ResolvedTeamMember } from '../../../../../src/shared/types';

const TEAM_NAME = 'relay-works-10';
const MEMBER_NAME = 'jack';
const LANE_ID = 'secondary:opencode:jack';
const GENERATED_AT = '2026-04-24T20:40:00.000Z';
const FIXTURE_PATH = path.resolve(
  process.cwd(),
  'test/fixtures/team/opencode/relay-works-10-jack-projection-transcript.json'
);

const tempDirs: string[] = [];

const apiState = {
  getMemberLogStream:
    vi.fn<
      (
        teamName: string,
        memberName: string,
        options?: MemberLogStreamRequestOptions
      ) => Promise<MemberLogStreamResponse>
    >(),
  setMemberLogStreamTracking: vi.fn<(teamName: string, enabled: boolean) => Promise<void>>(),
  getMemberRuntimeLogTail:
    vi.fn<
      (
        teamName: string,
        memberName: string,
        options: MemberRuntimeLogTailOptions
      ) => Promise<MemberRuntimeLogTailResponse>
    >(),
  onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(),
};

vi.mock('@renderer/api', () => ({
  api: {
    memberLogStream: {
      getMemberLogStream: (...args: Parameters<typeof apiState.getMemberLogStream>) =>
        apiState.getMemberLogStream(...args),
      setMemberLogStreamTracking: (
        ...args: Parameters<typeof apiState.setMemberLogStreamTracking>
      ) => apiState.setMemberLogStreamTracking(...args),
      getMemberRuntimeLogTail: (
        ...args: Parameters<typeof apiState.getMemberRuntimeLogTail>
      ) => apiState.getMemberRuntimeLogTail(...args),
    },
    teams: {
      onTeamChange: (...args: Parameters<typeof apiState.onTeamChange>) =>
        apiState.onTeamChange(...args),
    },
  },
}));

import { MemberLogStreamSection } from '../../../../../src/features/member-log-stream/renderer';

function flushMicrotasks(): Promise<void> {
  return Promise.resolve();
}

function flushAsyncWork(): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

async function waitForText(
  host: HTMLElement,
  predicate: (text: string) => boolean
): Promise<string> {
  let text = '';
  for (let attempt = 0; attempt < 25; attempt += 1) {
    await act(async () => {
      await flushAsyncWork();
    });
    text = host.textContent ?? '';
    if (predicate(text)) {
      return text;
    }
  }
  return text;
}

async function loadOpenCodeFixtureTranscript(): Promise<
  NonNullable<OpenCodeRuntimeTranscriptResponse['transcript']>
> {
  const parsed = JSON.parse(
    await readFile(FIXTURE_PATH, 'utf8')
  ) as OpenCodeRuntimeTranscriptResponse;
  if (parsed.providerId !== 'opencode' || !parsed.transcript) {
    throw new Error('Invalid OpenCode transcript fixture');
  }
  return parsed.transcript;
}

async function createClaudeTranscriptRef(): Promise<MemberLogFileRef> {
  const tempDir = await mkdtemp(path.join(os.tmpdir(), 'member-log-stream-e2e-'));
  tempDirs.push(tempDir);

  const filePath = path.join(tempDir, 'jack-claude-session.jsonl');
  const rows = [
    {
      parentUuid: null,
      isSidechain: true,
      userType: 'external',
      cwd: '/Users/tester/project',
      sessionId: 'claude-session-jack',
      version: '1.0.0',
      gitBranch: 'main',
      agentName: MEMBER_NAME,
      type: 'system',
      uuid: 'claude-init',
      timestamp: '2026-04-24T20:25:00.000Z',
      subtype: 'init',
      level: 'info',
      isMeta: false,
      content: 'member session started',
    },
    {
      parentUuid: 'claude-init',
      isSidechain: true,
      userType: 'external',
      cwd: '/Users/tester/project',
      sessionId: 'claude-session-jack',
      version: '1.0.0',
      gitBranch: 'main',
      agentName: MEMBER_NAME,
      type: 'user',
      uuid: 'claude-user-1',
      timestamp: '2026-04-24T20:25:01.000Z',
      isMeta: false,
      message: {
        role: 'user',
        content: 'Collect member-wide evidence for calculator behavior.',
      },
    },
    {
      parentUuid: 'claude-user-1',
      isSidechain: true,
      userType: 'external',
      cwd: '/Users/tester/project',
      sessionId: 'claude-session-jack',
      version: '1.0.0',
      gitBranch: 'main',
      agentName: MEMBER_NAME,
      type: 'assistant',
      uuid: 'claude-assistant-1',
      requestId: 'req-claude-1',
      timestamp: '2026-04-24T20:25:03.000Z',
      message: {
        role: 'assistant',
        id: 'msg-claude-1',
        type: 'message',
        model: 'claude-sonnet-4-5-20250929',
        content: [
          {
            type: 'text',
            text: 'Member-wide Claude transcript final note for Jack.',
          },
        ],
        stop_reason: null,
        stop_sequence: null,
        usage: { input_tokens: 12, output_tokens: 16 },
      },
    },
  ];

  await writeFile(filePath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
  const fileStat = await stat(filePath);

  return {
    memberName: MEMBER_NAME,
    sessionId: 'claude-session-jack',
    filePath,
    mtimeMs: fileStat.mtimeMs,
    sizeBytes: fileStat.size,
    messageCount: rows.length,
    kind: 'subagent',
  };
}

async function createFixtureUseCase(): Promise<{
  useCase: GetMemberLogStreamUseCase;
  getOpenCodeTranscript: ReturnType<typeof vi.fn>;
  findRecentMemberLogFileRefsByMember: ReturnType<typeof vi.fn>;
}> {
  const claudeRef = await createClaudeTranscriptRef();
  const openCodeTranscript = await loadOpenCodeFixtureTranscript();
  const findRecentMemberLogFileRefsByMember = vi.fn(() => Promise.resolve([claudeRef]));
  const getOpenCodeTranscript = vi.fn(() => Promise.resolve(openCodeTranscript));

  const chunkBuilder = new BoardTaskExactLogChunkBuilder();
  const sources = [
    new ClaudeMemberTranscriptStreamSource(
      { findRecentMemberLogFileRefsByMember } as never,
      new BoardTaskExactLogStrictParser(),
      chunkBuilder,
      { warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
    ),
    new OpenCodeMemberRuntimeStreamSource(
      { getOpenCodeTranscript } as never,
      chunkBuilder,
      { resolve: vi.fn(() => Promise.resolve('/Users/tester/agent_teams_orchestrator')) }
    ),
  ];

  return {
    useCase: new GetMemberLogStreamUseCase({
      sources,
      clock: { now: () => Date.parse(GENERATED_AT) },
      logger: { warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
    }),
    getOpenCodeTranscript,
    findRecentMemberLogFileRefsByMember,
  };
}

function createMember(): ResolvedTeamMember {
  return {
    name: MEMBER_NAME,
    status: 'idle',
    currentTaskId: null,
    taskCount: 2,
    lastActiveAt: '2026-04-24T20:34:00.000Z',
    messageCount: 12,
    color: 'blue',
    providerId: 'opencode',
    laneId: LANE_ID,
    laneKind: 'secondary',
    laneOwnerProviderId: 'opencode',
  };
}

function stubMatchMedia(): void {
  const matchMedia = vi.fn((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    addListener: vi.fn(),
    removeListener: vi.fn(),
    dispatchEvent: vi.fn(),
  }));
  vi.stubGlobal('matchMedia', matchMedia);
}

function expectCapturedResponse(
  value: MemberLogStreamResponse | null
): MemberLogStreamResponse {
  expect(value).not.toBeNull();
  return value!;
}

describe('MemberLogStreamSection real fixture e2e', () => {
  afterEach(async () => {
    document.body.innerHTML = '';
    apiState.getMemberLogStream.mockReset();
    apiState.setMemberLogStreamTracking.mockReset();
    apiState.getMemberRuntimeLogTail.mockReset();
    apiState.onTeamChange.mockReset();
    vi.unstubAllGlobals();
    await Promise.all(
      tempDirs.splice(0, tempDirs.length).map((dirPath) =>
        rm(dirPath, { recursive: true, force: true })
      )
    );
  });

  it('renders member-wide Claude transcript and OpenCode runtime logs through the member Logs UI', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    stubMatchMedia();
    apiState.onTeamChange.mockImplementation(() => () => undefined);
    apiState.setMemberLogStreamTracking.mockResolvedValue(undefined);
    apiState.getMemberRuntimeLogTail.mockResolvedValue({
      kind: 'stdout',
      content: 'process stdout line',
      truncated: false,
      bytesRead: 19,
      missing: false,
    });

    const { useCase, getOpenCodeTranscript, findRecentMemberLogFileRefsByMember } =
      await createFixtureUseCase();
    const capturedResponseRef: { current: MemberLogStreamResponse | null } = { current: null };
    apiState.getMemberLogStream.mockImplementation(async (teamName, memberName, options) => {
      const response = await useCase.execute({
        teamName,
        memberName,
        limitSegments: options?.limitSegments,
        laneId: options?.laneId,
        forceRefresh: options?.forceRefresh,
      });
      capturedResponseRef.current = response;
      return response;
    });

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(
          TooltipProvider,
          null,
          React.createElement(MemberLogStreamSection, {
            teamName: TEAM_NAME,
            member: createMember(),
          })
        )
      );
      await flushMicrotasks();
    });

    const text = await waitForText(host, (content) =>
      content.includes('Member-wide Claude transcript final note for Jack.')
    );

    expect(text).toContain('Logs');
    expect(text).toContain('Member-scoped transcript and runtime logs');
    expect(text).toContain('Claude transcript');
    expect(text).toContain('OpenCode runtime');
    expect(text).toContain('Calculator behavior');
    expect(text).toContain('Logic smoke check');
    expect(text).toContain('Collect member-wide evidence for calculator behavior.');

    const capturedResponse = expectCapturedResponse(capturedResponseRef.current);
    expect(capturedResponse).toMatchObject({
      source: 'member_mixed_runtime',
      defaultFilter: 'member:jack',
      generatedAt: GENERATED_AT,
      metadata: {
        scannedTranscriptFileCount: 1,
        includedTranscriptFileCount: 1,
      },
    });
    expect(capturedResponse.coverage).toEqual(
      expect.arrayContaining([
        { provider: 'claude_transcript', status: 'included' },
        { provider: 'opencode_runtime', status: 'included' },
      ])
    );
    expect(JSON.stringify(capturedResponse.segments)).toContain('Keyboard handlers added');
    expect(apiState.getMemberLogStream).toHaveBeenCalledWith(
      TEAM_NAME,
      MEMBER_NAME,
      expect.objectContaining({
        limitSegments: 30,
        laneId: LANE_ID,
      })
    );
    expect(findRecentMemberLogFileRefsByMember).toHaveBeenCalledWith(
      TEAM_NAME,
      [MEMBER_NAME],
      expect.objectContaining({ forceRefresh: false })
    );
    expect(getOpenCodeTranscript).toHaveBeenCalledWith(
      '/Users/tester/agent_teams_orchestrator',
      expect.objectContaining({
        teamId: TEAM_NAME,
        memberName: MEMBER_NAME,
        laneId: LANE_ID,
        limit: 400,
        timeoutMs: 5_000,
      })
    );

    await act(async () => {
      root.unmount();
      await flushMicrotasks();
    });

    expect(apiState.setMemberLogStreamTracking).toHaveBeenCalledWith(TEAM_NAME, true);
    expect(apiState.setMemberLogStreamTracking).toHaveBeenCalledWith(TEAM_NAME, false);
  });

  it('loads bounded process runtime logs after switching the Logs UI to Process', async () => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    stubMatchMedia();
    apiState.onTeamChange.mockImplementation(() => () => undefined);
    apiState.setMemberLogStreamTracking.mockResolvedValue(undefined);
    apiState.getMemberLogStream.mockResolvedValue({
      participants: [],
      defaultFilter: 'all',
      segments: [],
      source: 'member_empty',
      coverage: [],
      warnings: [],
      truncated: false,
      generatedAt: GENERATED_AT,
      metadata: {
        scannedTranscriptFileCount: 0,
        includedTranscriptFileCount: 0,
        droppedSegmentCount: 0,
        droppedChunkCount: 0,
        droppedMessageCount: 0,
      },
    });
    apiState.getMemberRuntimeLogTail.mockResolvedValue({
      kind: 'stdout',
      content: 'process stdout line',
      truncated: false,
      bytesRead: 19,
      missing: false,
    });

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(
          TooltipProvider,
          null,
          React.createElement(MemberLogStreamSection, {
            teamName: TEAM_NAME,
            member: createMember(),
          })
        )
      );
      await flushMicrotasks();
    });

    const processButton = Array.from(host.querySelectorAll('button')).find(
      (button) => button.textContent?.trim() === 'Process'
    ) as HTMLButtonElement | undefined;
    expect(processButton).toBeTruthy();

    await act(async () => {
      processButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await flushAsyncWork();
    });

    await waitForText(host, (content) => content.includes('process stdout line'));
    expect(apiState.getMemberRuntimeLogTail).toHaveBeenCalledWith(TEAM_NAME, MEMBER_NAME, {
      kind: 'stdout',
      maxBytes: 128 * 1024,
      forceRefresh: true,
    });

    await act(async () => {
      root.unmount();
      await flushMicrotasks();
    });
  });
});
