import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';

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

import { TeamTranscriptProjectResolver } from '../../../../src/main/services/team/TeamTranscriptProjectResolver';
import { encodePath, setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';

import type { TeamConfig } from '../../../../src/shared/types/team';

describe('TeamTranscriptProjectResolver', () => {
  let tmpDir: string | null = null;

  afterEach(async () => {
    setClaudeBasePathOverride(null);
    if (tmpDir) {
      await fs.rm(tmpDir, { recursive: true, force: true });
      tmpDir = null;
    }
  });

  async function setupClaudeRoot(): Promise<string> {
    tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-transcript-project-resolver-'));
    setClaudeBasePathOverride(tmpDir);
    await fs.mkdir(path.join(tmpDir, 'teams'), { recursive: true });
    await fs.mkdir(path.join(tmpDir, 'projects'), { recursive: true });
    return tmpDir;
  }

  async function writeTeamConfig(teamName: string, config: TeamConfig): Promise<void> {
    const teamDir = path.join(tmpDir!, 'teams', teamName);
    await fs.mkdir(teamDir, { recursive: true });
    await fs.writeFile(path.join(teamDir, 'config.json'), JSON.stringify(config, null, 2), 'utf8');
  }

  async function readTeamConfig(teamName: string): Promise<TeamConfig> {
    const raw = await fs.readFile(path.join(tmpDir!, 'teams', teamName, 'config.json'), 'utf8');
    return JSON.parse(raw) as TeamConfig;
  }

  async function createSessionFile(
    projectPath: string,
    sessionId: string,
    cwd: string = projectPath
  ): Promise<{ projectDir: string; jsonlPath: string }> {
    const projectDir = path.join(tmpDir!, 'projects', encodePath(projectPath));
    await fs.mkdir(projectDir, { recursive: true });
    const jsonlPath = path.join(projectDir, `${sessionId}.jsonl`);
    await fs.writeFile(
      jsonlPath,
      `${JSON.stringify({
        type: 'assistant',
        timestamp: '2026-04-18T10:00:00.000Z',
        cwd,
        message: {
          role: 'assistant',
          content: [{ type: 'text', text: 'Resolver probe output' }],
        },
      })}\n`,
      'utf8'
    );
    return { projectDir, jsonlPath };
  }

  async function createSessionFileInProjectDir(
    projectDirName: string,
    sessionId: string,
    cwd: string
  ): Promise<{ projectDir: string; jsonlPath: string }> {
    const projectDir = path.join(tmpDir!, 'projects', projectDirName);
    await fs.mkdir(projectDir, { recursive: true });
    const jsonlPath = path.join(projectDir, `${sessionId}.jsonl`);
    await fs.writeFile(
      jsonlPath,
      `${JSON.stringify({
        type: 'assistant',
        timestamp: '2026-04-18T10:00:00.000Z',
        cwd,
        message: {
          role: 'assistant',
          content: [{ type: 'text', text: 'Resolver probe output' }],
        },
      })}\n`,
      'utf8'
    );
    return { projectDir, jsonlPath };
  }

  async function createTeamAwareSessionFile(
    projectPath: string,
    sessionId: string,
    teamName: string,
    mode: 'text' | 'nested'
  ): Promise<{ projectDir: string; jsonlPath: string }> {
    const projectDir = path.join(tmpDir!, 'projects', encodePath(projectPath));
    await fs.mkdir(projectDir, { recursive: true });
    const jsonlPath = path.join(projectDir, `${sessionId}.jsonl`);
    const lines =
      mode === 'text'
        ? [
            {
              type: 'user',
              timestamp: '2026-04-18T10:00:00.000Z',
              cwd: projectPath,
              message: {
                role: 'user',
                content: [
                  {
                    type: 'text',
                    text: `Current durable team context:\n- Team name: ${teamName}\n- You are the live team lead "team-lead"`,
                  },
                ],
              },
            },
          ]
        : [
            {
              type: 'assistant',
              timestamp: '2026-04-18T10:00:00.000Z',
              cwd: projectPath,
              message: {
                role: 'assistant',
                content: [
                  {
                    type: 'tool_use',
                    id: 'call_probe',
                    name: 'mcp__agent-teams__task_create_from_message',
                    input: {
                      teamName,
                      subject: 'Probe task',
                    },
                  },
                ],
              },
            },
          ];

    await fs.writeFile(
      jsonlPath,
      `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`,
      'utf8'
    );
    return { projectDir, jsonlPath };
  }

  it('uses snapshot-capable config readers for resolver observations', async () => {
    await setupClaudeRoot();
    const { projectDir } = await createSessionFile('/repo/current', 'lead-session-1');
    const getConfig = vi.fn(async () => {
      throw new Error('verified config read should not be used for transcript observations');
    });
    const getConfigSnapshot = vi.fn(async () => ({
      name: 'My Team',
      projectPath: '/repo/current',
      leadSessionId: 'lead-session-1',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    }));
    const resolver = new TeamTranscriptProjectResolver({
      getConfig,
      getConfigSnapshot,
    });

    const context = await resolver.getContext('my-team');

    expect(context?.projectDir).toBe(projectDir);
    expect(getConfigSnapshot).toHaveBeenCalledWith('my-team');
    expect(getConfig).not.toHaveBeenCalled();
  });

  it('resolves live base context from cheap projectPath evidence without session discovery', async () => {
    await setupClaudeRoot();

    const teamName = 'live-base-team';
    const projectPath = '/Users/test/live-base';
    const projectDir = path.join(tmpDir!, 'projects', encodePath(projectPath));
    await fs.mkdir(projectDir, { recursive: true });
    const getConfig = vi.fn(async () => {
      throw new Error('verified config read should not be used for live base context');
    });
    const getConfigSnapshot = vi.fn(async () => ({
      name: teamName,
      projectPath,
      leadSessionId: 'lead-session',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    }));
    const resolver = new TeamTranscriptProjectResolver({
      getConfig,
      getConfigSnapshot,
    });

    const context = await resolver.getLiveBaseContext(teamName, { forceRefresh: true });

    expect(context?.projectDir).toBe(projectDir);
    expect(context?.config.projectPath).toBe(projectPath);
    expect(getConfigSnapshot).toHaveBeenCalledWith(teamName);
    expect(getConfig).not.toHaveBeenCalled();
  });

  it('repairs stale projectPath when exact leadSessionId exists only in the renamed project', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const leadSessionId = 'lead-1';
    const staleProjectDir = path.join(tmpDir!, 'projects', encodePath(staleProjectPath));
    await fs.mkdir(staleProjectDir, { recursive: true });
    const repaired = await createSessionFile(repairedProjectPath, leadSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      leadSessionId,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context).not.toBeNull();
    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
    expect(persisted.projectPath).toBe(repairedProjectPath);
    expect(persisted.projectPathHistory).toEqual(expect.arrayContaining([staleProjectPath]));
  });

  it('keeps the current projectPath when it already contains the exact session', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const currentProjectPath = '/Users/test/hookplex';
    const alternateProjectPath = '/Users/test/plugin-kit-ai';
    const leadSessionId = 'lead-1';
    const current = await createSessionFile(currentProjectPath, leadSessionId);
    await createSessionFile(alternateProjectPath, leadSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: currentProjectPath,
      projectPathHistory: [alternateProjectPath],
      leadSessionId,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: alternateProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context?.projectDir).toBe(current.projectDir);
    expect(context?.config.projectPath).toBe(currentProjectPath);
    expect(persisted.projectPath).toBe(currentProjectPath);
    expect(persisted.projectPathHistory).toEqual([alternateProjectPath]);
  });

  it('falls back to exact sessionHistory ids when leadSessionId file is missing', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const historicalSessionId = 'lead-old';
    await fs.mkdir(path.join(tmpDir!, 'projects', encodePath(staleProjectPath)), {
      recursive: true,
    });
    const repaired = await createSessionFile(repairedProjectPath, historicalSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      leadSessionId: 'lead-missing',
      sessionHistory: [historicalSessionId],
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
    expect(persisted.projectPath).toBe(repairedProjectPath);
  });

  it('prefers the newest sessionHistory match when leadSessionId is missing', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const olderSessionId = 'lead-old';
    const newerSessionId = 'lead-new';
    await createSessionFile(staleProjectPath, olderSessionId);
    const repaired = await createSessionFile(repairedProjectPath, newerSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      leadSessionId: 'lead-missing',
      sessionHistory: [olderSessionId, newerSessionId],
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
  });

  it('does not let an old sessionHistory match block repair when the current leadSessionId exists elsewhere', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const leadSessionId = 'lead-current';
    const historicalSessionId = 'lead-old';
    await createSessionFile(staleProjectPath, historicalSessionId);
    const repaired = await createSessionFile(repairedProjectPath, leadSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      leadSessionId,
      sessionHistory: [historicalSessionId],
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
    expect(persisted.projectPath).toBe(repairedProjectPath);
  });

  it('picks the best exact session match across dir variants for the same projectPath', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const projectPath = '/Users/test/plugin_kit_ai';
    const staleSessionId = 'lead-old';
    const currentSessionId = 'lead-current';
    await createSessionFile(projectPath, staleSessionId);
    const repaired = await createSessionFileInProjectDir(
      encodePath(projectPath).replace(/_/g, '-'),
      currentSessionId,
      projectPath
    );

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath,
      leadSessionId: currentSessionId,
      sessionHistory: [staleSessionId],
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: projectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
  });

  it('does not self-heal when an alternate configured match is not unique across projects scan', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const configuredProjectPath = '/Users/test/plugin-kit-ai';
    const duplicateProjectPath = '/Users/test/plugin-kit-ai-copy';
    const leadSessionId = 'lead-1';
    const staleProjectDir = path.join(tmpDir!, 'projects', encodePath(staleProjectPath));
    await fs.mkdir(staleProjectDir, { recursive: true });
    await createSessionFile(configuredProjectPath, leadSessionId);
    await createSessionFile(duplicateProjectPath, leadSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      projectPathHistory: [configuredProjectPath],
      leadSessionId,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: configuredProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const warnSpy = vi.mocked(console.warn);
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context?.projectDir).toBe(staleProjectDir);
    expect(context?.config.projectPath).toBe(staleProjectPath);
    expect(persisted.projectPath).toBe(staleProjectPath);
    expect(warnSpy.mock.calls).toEqual(
      expect.arrayContaining([
        expect.arrayContaining([
          expect.stringContaining(
            'Transcript project resolution ambiguous across exact-session candidates'
          ),
        ]),
      ])
    );
    warnSpy.mockClear();
  });

  it('does not self-heal when full scan finds multiple equally valid session matches', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const staleProjectPath = '/Users/test/hookplex';
    const leadSessionId = 'lead-1';
    const staleProjectDir = path.join(tmpDir!, 'projects', encodePath(staleProjectPath));
    await fs.mkdir(staleProjectDir, { recursive: true });
    await createSessionFile('/Users/test/plugin-kit-ai', leadSessionId);
    await createSessionFile('/Users/test/plugin-kit-ai-copy', leadSessionId);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      leadSessionId,
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const warnSpy = vi.mocked(console.warn);
    const context = await resolver.getContext(teamName);
    const persisted = await readTeamConfig(teamName);

    expect(context?.projectDir).toBe(staleProjectDir);
    expect(context?.config.projectPath).toBe(staleProjectPath);
    expect(persisted.projectPath).toBe(staleProjectPath);
    expect(warnSpy.mock.calls).toEqual(
      expect.arrayContaining([
        expect.arrayContaining([
          expect.stringContaining(
            'Transcript project resolution ambiguous across exact-session candidates'
          ),
        ]),
      ])
    );
    warnSpy.mockClear();
  });

  it('falls back to an existing alternate dir candidate when no session ids are known yet', async () => {
    await setupClaudeRoot();

    const teamName = 'my-team';
    const projectPath = '/Users/test/plugin_kit_ai';
    const alternateDir = encodePath(projectPath).replace(/_/g, '-');
    const fallback = await createSessionFileInProjectDir(alternateDir, 'lead-1', projectPath);

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: projectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);

    expect(context?.projectDir).toBe(fallback.projectDir);
    expect(context?.config.projectPath).toBe(projectPath);
  });

  it('prefers a later candidate when the transcript text explicitly names the team and the stale project dir still exists', async () => {
    await setupClaudeRoot();

    const teamName = 'vector-room-55555551';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const staleProjectDir = path.join(tmpDir!, 'projects', encodePath(staleProjectPath));
    await fs.mkdir(staleProjectDir, { recursive: true });
    const repaired = await createTeamAwareSessionFile(
      repairedProjectPath,
      'lead-1',
      teamName,
      'text'
    );

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
  });

  it('recognizes nested tool input teamName during no-session fallback', async () => {
    await setupClaudeRoot();

    const teamName = 'vector-room-55555551';
    const staleProjectPath = '/Users/test/hookplex';
    const repairedProjectPath = '/Users/test/plugin-kit-ai';
    const staleProjectDir = path.join(tmpDir!, 'projects', encodePath(staleProjectPath));
    await fs.mkdir(staleProjectDir, { recursive: true });
    const repaired = await createTeamAwareSessionFile(
      repairedProjectPath,
      'lead-1',
      teamName,
      'nested'
    );

    await writeTeamConfig(teamName, {
      name: 'My Team',
      projectPath: staleProjectPath,
      members: [{ name: 'team-lead', agentType: 'team-lead', cwd: repairedProjectPath }],
    });

    const resolver = new TeamTranscriptProjectResolver();
    const context = await resolver.getContext(teamName);

    expect(context?.projectDir).toBe(repaired.projectDir);
    expect(context?.config.projectPath).toBe(repairedProjectPath);
  });

  it('bounds root session discovery by team lifecycle in fast preview context', async () => {
    await setupClaudeRoot();

    const teamName = 'fast-preview-team';
    const projectPath = '/Users/test/fast-preview';
    const createdAt = Date.parse('2026-04-18T12:00:00.000Z');
    const leadSessionId = 'lead-fast';
    const lead = await createTeamAwareSessionFile(projectPath, leadSessionId, teamName, 'text');
    const recent = await createTeamAwareSessionFile(
      projectPath,
      'recent-member-session',
      teamName,
      'text'
    );
    const old = await createTeamAwareSessionFile(
      projectPath,
      'old-member-session',
      teamName,
      'text'
    );
    await fs.utimes(lead.jsonlPath, new Date(createdAt + 60_000), new Date(createdAt + 60_000));
    await fs.utimes(
      recent.jsonlPath,
      new Date(createdAt + 5 * 60_000),
      new Date(createdAt + 5 * 60_000)
    );
    await fs.utimes(
      old.jsonlPath,
      new Date(createdAt - 25 * 60 * 60_000),
      new Date(createdAt - 25 * 60 * 60_000)
    );

    await writeTeamConfig(teamName, {
      name: 'Fast Preview Team',
      createdAt,
      projectPath,
      leadSessionId,
      members: [
        { name: 'team-lead', agentType: 'team-lead', joinedAt: createdAt, cwd: projectPath },
        { name: 'alice', agentType: 'general-purpose', joinedAt: createdAt + 5 * 60_000 },
      ],
    } as TeamConfig);

    const resolver = new TeamTranscriptProjectResolver();
    const fastContext = await resolver.getContext(teamName, {
      forceRefresh: true,
      includeTeamSubagentSessionDiscovery: false,
    });
    const fullContext = await resolver.getContext(teamName, { forceRefresh: true });

    expect(fastContext?.projectDir).toBe(lead.projectDir);
    expect(fastContext?.sessionIds).toEqual(expect.arrayContaining([leadSessionId]));
    expect(fastContext?.sessionIds).toContain('recent-member-session');
    expect(fastContext?.sessionIds).not.toContain('old-member-session');
    expect(fullContext?.sessionIds).toContain('old-member-session');
  });
});
