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

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

const hoisted = vi.hoisted(() => ({
  teamsBase: '',
  backupsBase: '',
  appDataPath: '',
  tasksBase: '',
}));

vi.mock('../../../../src/main/utils/pathDecoder', () => ({
  getTeamsBasePath: () => hoisted.teamsBase,
  getBackupsBasePath: () => hoisted.backupsBase,
  getAppDataPath: () => hoisted.appDataPath,
  getTasksBasePath: () => hoisted.tasksBase,
}));

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

describe('TeamBackupService', () => {
  let tempDir = '';

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-backup-service-'));
    hoisted.teamsBase = path.join(tempDir, 'teams');
    hoisted.backupsBase = path.join(tempDir, 'backups');
    hoisted.appDataPath = path.join(tempDir, 'app-data');
    hoisted.tasksBase = path.join(tempDir, 'tasks');

    await fs.mkdir(hoisted.teamsBase, { recursive: true });
    await fs.mkdir(hoisted.backupsBase, { recursive: true });
    await fs.mkdir(hoisted.appDataPath, { recursive: true });
    await fs.mkdir(hoisted.tasksBase, { recursive: true });
  });

  afterEach(async () => {
    hoisted.teamsBase = '';
    hoisted.backupsBase = '';
    hoisted.appDataPath = '';
    hoisted.tasksBase = '';
    if (tempDir) {
      await fs.rm(tempDir, { recursive: true, force: true });
    }
  });

  it('backs up and restores additive mixed-lane metadata and launch snapshots', async () => {
    const service = new TeamBackupService();
    const teamName = 'mixed-team';
    const teamDir = path.join(hoisted.teamsBase, teamName);
    await fs.mkdir(teamDir, { recursive: true });

    const config = {
      name: 'Mixed Team',
      projectPath: '/tmp/project',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    };
    const teamMeta = {
      version: 1,
      cwd: '/tmp/project',
      providerId: 'codex',
      providerBackendId: 'codex-native',
      fastMode: 'off',
      createdAt: Date.now(),
    };
    const membersMeta = {
      version: 1,
      providerBackendId: 'codex-native',
      members: [
        { name: 'alice', providerId: 'codex', role: 'reviewer' },
        {
          name: 'tom',
          providerId: 'opencode',
          providerBackendId: 'opencode-cli',
          model: 'minimax-m2.5-free',
          fastMode: 'inherit',
          role: 'developer',
        },
      ],
    };
    const launchState = {
      version: 2,
      teamName,
      updatedAt: '2026-04-22T12:00:00.000Z',
      launchPhase: 'active',
      expectedMembers: ['alice', 'tom'],
      bootstrapExpectedMembers: ['alice'],
      members: {
        alice: {
          name: 'alice',
          providerId: 'codex',
          providerBackendId: 'codex-native',
          laneId: 'primary',
          laneKind: 'primary',
          laneOwnerProviderId: 'codex',
          launchState: 'confirmed_alive',
          agentToolAccepted: true,
          runtimeAlive: true,
          bootstrapConfirmed: true,
          hardFailure: false,
          lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
        },
        tom: {
          name: 'tom',
          providerId: 'opencode',
          providerBackendId: 'opencode-cli',
          laneId: 'secondary:opencode:tom',
          laneKind: 'secondary',
          laneOwnerProviderId: 'opencode',
          launchState: 'starting',
          agentToolAccepted: true,
          runtimeAlive: false,
          bootstrapConfirmed: false,
          hardFailure: false,
          lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
        },
      },
      summary: {
        confirmedCount: 1,
        pendingCount: 1,
        failedCount: 0,
        runtimeAlivePendingCount: 0,
      },
      teamLaunchState: 'partial_pending',
    };
    const launchSummary = {
      version: 1,
      teamName,
      updatedAt: '2026-04-22T12:00:00.000Z',
      mixedAware: true,
      expectedMemberCount: 2,
      confirmedMemberCount: 1,
      pendingCount: 1,
      failedCount: 0,
      teamLaunchState: 'partial_pending',
      launchUpdatedAt: '2026-04-22T12:00:00.000Z',
    };
    const runtimeLaneDir = path.join(
      teamDir,
      '.opencode-runtime',
      'lanes',
      encodeURIComponent('secondary:opencode:tom')
    );
    const runtimeLaneIndex = {
      version: 1,
      updatedAt: '2026-04-22T12:00:00.000Z',
      lanes: {
        'secondary:opencode:tom': {
          laneId: 'secondary:opencode:tom',
          state: 'active',
          updatedAt: '2026-04-22T12:00:00.000Z',
          diagnostics: [],
        },
      },
    };
    const runtimeManifest = {
      schemaVersion: 1,
      highWatermark: 12,
      activeRunId: 'lane-run-1',
      capabilitySnapshotId: 'cap-1',
    };

    await fs.writeFile(path.join(teamDir, 'config.json'), JSON.stringify(config), 'utf8');
    await fs.writeFile(path.join(teamDir, 'team.meta.json'), JSON.stringify(teamMeta), 'utf8');
    await fs.writeFile(
      path.join(teamDir, 'members.meta.json'),
      JSON.stringify(membersMeta),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'launch-state.json'),
      JSON.stringify(launchState),
      'utf8'
    );
    await fs.writeFile(
      path.join(teamDir, 'launch-summary.json'),
      JSON.stringify(launchSummary),
      'utf8'
    );
    await fs.mkdir(runtimeLaneDir, { recursive: true });
    await fs.writeFile(
      path.join(teamDir, '.opencode-runtime', 'lanes.json'),
      JSON.stringify(runtimeLaneIndex),
      'utf8'
    );
    await fs.writeFile(
      path.join(runtimeLaneDir, 'runtime-store-manifest.json'),
      JSON.stringify(runtimeManifest),
      'utf8'
    );

    await service.initialize();
    await service.backupTeam(teamName);

    await fs.rm(teamDir, { recursive: true, force: true });

    const restored = await service.restoreIfNeeded();
    service.dispose();

    expect(restored).toContain(teamName);

    const restoredMembersMeta = JSON.parse(
      await fs.readFile(path.join(teamDir, 'members.meta.json'), 'utf8')
    );
    const restoredLaunchState = JSON.parse(
      await fs.readFile(path.join(teamDir, 'launch-state.json'), 'utf8')
    );
    const restoredLaunchSummary = JSON.parse(
      await fs.readFile(path.join(teamDir, 'launch-summary.json'), 'utf8')
    );
    const restoredTeamMeta = JSON.parse(
      await fs.readFile(path.join(teamDir, 'team.meta.json'), 'utf8')
    );
    const restoredRuntimeLaneIndex = JSON.parse(
      await fs.readFile(path.join(teamDir, '.opencode-runtime', 'lanes.json'), 'utf8')
    );
    const restoredRuntimeManifest = JSON.parse(
      await fs.readFile(path.join(runtimeLaneDir, 'runtime-store-manifest.json'), 'utf8')
    );

    expect(restoredTeamMeta.providerId).toBe('codex');
    expect(restoredMembersMeta.members).toEqual(membersMeta.members);
    expect(restoredLaunchState.bootstrapExpectedMembers).toEqual(['alice']);
    expect(restoredLaunchState.members.tom.laneKind).toBe('secondary');
    expect(restoredLaunchState.members.tom.laneOwnerProviderId).toBe('opencode');
    expect(restoredLaunchSummary.mixedAware).toBe(true);
    expect(restoredLaunchSummary.teamLaunchState).toBe('partial_pending');
    expect(restoredRuntimeLaneIndex.lanes['secondary:opencode:tom'].state).toBe('active');
    expect(restoredRuntimeManifest.activeRunId).toBe('lane-run-1');
  });

  it('skips quarantined and temporary OpenCode runtime files during backup', async () => {
    const service = new TeamBackupService();
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
    const teamName = 'runtime-quarantine-team';
    const teamDir = path.join(hoisted.teamsBase, teamName);
    const runtimeDir = path.join(teamDir, '.opencode-runtime');
    const runtimeLaneIndex = {
      version: 1,
      updatedAt: '2026-04-22T12:00:00.000Z',
      lanes: {
        'secondary:opencode:tom': {
          laneId: 'secondary:opencode:tom',
          state: 'active',
          updatedAt: '2026-04-22T12:00:00.000Z',
        },
      },
    };

    try {
      await fs.mkdir(runtimeDir, { recursive: true });
      await fs.writeFile(
        path.join(teamDir, 'config.json'),
        JSON.stringify({ name: 'Runtime Quarantine Team' }),
        'utf8'
      );
      await fs.writeFile(
        path.join(runtimeDir, 'lanes.json'),
        JSON.stringify(runtimeLaneIndex),
        'utf8'
      );
      await fs.writeFile(
        path.join(runtimeDir, 'lanes.invalid.123.json'),
        '{"version":1}\n}',
        'utf8'
      );
      await fs.writeFile(path.join(runtimeDir, '.tmp.deadbeef'), '{"partial":', 'utf8');

      await service.initialize();
      await service.backupTeam(teamName);

      const backupRuntimeDir = path.join(
        hoisted.backupsBase,
        'teams',
        teamName,
        '.opencode-runtime'
      );
      await expect(fs.readFile(path.join(backupRuntimeDir, 'lanes.json'), 'utf8')).resolves.toBe(
        JSON.stringify(runtimeLaneIndex)
      );
      await expect(
        fs.stat(path.join(backupRuntimeDir, 'lanes.invalid.123.json'))
      ).rejects.toMatchObject({ code: 'ENOENT' });
      await expect(fs.stat(path.join(backupRuntimeDir, '.tmp.deadbeef'))).rejects.toMatchObject({
        code: 'ENOENT',
      });

      const manifest = JSON.parse(
        await fs.readFile(
          path.join(hoisted.backupsBase, 'teams', teamName, 'manifest.json'),
          'utf8'
        )
      ) as { fileStats: Record<string, unknown> };
      expect(
        Object.prototype.hasOwnProperty.call(
          manifest.fileStats,
          '.opencode-runtime/lanes.invalid.123.json'
        )
      ).toBe(false);
      expect(
        Object.prototype.hasOwnProperty.call(
          manifest.fileStats,
          '.opencode-runtime/.tmp.deadbeef'
        )
      ).toBe(false);
      expect(
        warnSpy.mock.calls.some((args) =>
          args.some((arg) => String(arg).includes('Skipping invalid JSON'))
        )
      ).toBe(false);
    } finally {
      service.dispose();
      warnSpy.mockRestore();
    }
  });

  it('backs up member-scoped work sync files', async () => {
    const service = new TeamBackupService();
    const teamName = 'member-work-sync-team';
    const teamDir = path.join(hoisted.teamsBase, teamName);
    const memberDir = path.join(teamDir, 'members', 'jack');
    const workSyncDir = path.join(memberDir, '.member-work-sync');
    const status = {
      teamName,
      memberName: 'jack',
      state: 'caught_up',
      evaluatedAt: '2026-04-30T12:00:00.000Z',
      agenda: { fingerprint: 'abc123', items: [] },
    };

    try {
      await fs.mkdir(workSyncDir, { recursive: true });
      await fs.writeFile(
        path.join(teamDir, 'config.json'),
        JSON.stringify({ name: 'Member Work Sync Team' }),
        'utf8'
      );
      await fs.writeFile(
        path.join(memberDir, 'member.meta.json'),
        JSON.stringify({
          schemaVersion: 1,
          memberName: 'jack',
          memberKey: 'jack',
          updatedAt: '2026-04-30T12:00:00.000Z',
        }),
        'utf8'
      );
      await fs.writeFile(
        path.join(workSyncDir, 'status.json'),
        JSON.stringify({ schemaVersion: 2, status }),
        'utf8'
      );
      await fs.writeFile(
        path.join(workSyncDir, 'journal.jsonl'),
        `${JSON.stringify({
          schemaVersion: 1,
          timestamp: '2026-04-30T12:00:00.000Z',
          teamName,
          memberName: 'jack',
          event: 'status_written',
          source: 'test',
        })}\n`,
        'utf8'
      );
      await fs.writeFile(path.join(workSyncDir, '.tmp.deadbeef'), '{"partial":', 'utf8');
      await fs.writeFile(path.join(workSyncDir, 'journal.jsonl.lock'), '123\n', 'utf8');

      await service.initialize();
      await service.backupTeam(teamName);

      const backupMemberDir = path.join(hoisted.backupsBase, 'teams', teamName, 'members', 'jack');
      await expect(fs.readFile(path.join(backupMemberDir, 'member.meta.json'), 'utf8')).resolves.toBe(
        JSON.stringify({
          schemaVersion: 1,
          memberName: 'jack',
          memberKey: 'jack',
          updatedAt: '2026-04-30T12:00:00.000Z',
        })
      );
      await expect(
        fs.readFile(path.join(backupMemberDir, '.member-work-sync', 'status.json'), 'utf8')
      ).resolves.toBe(JSON.stringify({ schemaVersion: 2, status }));
      await expect(
        fs.readFile(path.join(backupMemberDir, '.member-work-sync', 'journal.jsonl'), 'utf8')
      ).resolves.toContain('"event":"status_written"');
      await expect(
        fs.stat(path.join(backupMemberDir, '.member-work-sync', '.tmp.deadbeef'))
      ).rejects.toMatchObject({ code: 'ENOENT' });
      await expect(
        fs.stat(path.join(backupMemberDir, '.member-work-sync', 'journal.jsonl.lock'))
      ).rejects.toMatchObject({ code: 'ENOENT' });
    } finally {
      service.dispose();
    }
  });
});
