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

import type { SnippetDiff } from '@shared/types';

vi.mock('fs/promises', async (importOriginal) => {
  const actual = await importOriginal<typeof import('fs/promises')>();
  const access = vi.fn();
  const readFile = vi.fn();
  return {
    ...actual,
    access,
    readFile,
    // ESM interop: some code paths expect a default export
    default: { ...actual, access, readFile },
  };
});

describe('FileContentResolver', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
    vi.useRealTimers();
  });

  it('treats empty on-disk content as valid for write-new reconstruction', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);

    const snippets: SnippetDiff[] = [
      {
        toolUseId: 't1',
        filePath: '/tmp/empty-new.txt',
        toolName: 'Write',
        type: 'write-new',
        oldString: '',
        newString: '',
        replaceAll: false,
        timestamp: new Date().toISOString(),
        isError: false,
      },
    ];

    const content = await resolver.getFileContent('team', 'member', '/tmp/empty-new.txt', snippets);
    expect(content.isNewFile).toBe(true);
    expect(content.originalFullContent).toBe('');
    expect(content.modifiedFullContent).toBe('');
    expect(content.contentSource).toBe('snippet-reconstruction');
  });

  it('maps ledger create original content to empty string without disk reconstruction', async () => {
    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');
    const resolver = new FileContentResolver({ findMemberLogPaths: vi.fn() } as any);

    const content = await resolver.getFileContent('team', 'member', '/tmp/ledger-create.txt', [
      {
        toolUseId: 'ledger-1',
        filePath: '/tmp/ledger-create.txt',
        toolName: 'Bash',
        type: 'shell-snapshot',
        oldString: '',
        newString: 'created\n',
        replaceAll: false,
        timestamp: '2026-03-01T10:00:00.000Z',
        isError: false,
        ledger: {
          eventId: 'event-1',
          source: 'ledger-snapshot',
          confidence: 'high',
          originalFullContent: null,
          modifiedFullContent: 'created\n',
          beforeHash: null,
          afterHash: 'hash',
          operation: 'create',
          beforeState: { exists: false },
          afterState: { exists: true, sha256: 'hash' },
        },
      },
    ]);

    expect(content.originalFullContent).toBe('');
    expect(content.modifiedFullContent).toBe('created\n');
    expect(content.contentSource).toBe('ledger-snapshot');
  });

  it('maps ledger delete modified content to empty string for diff display', async () => {
    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');
    const resolver = new FileContentResolver({ findMemberLogPaths: vi.fn() } as any);

    const content = await resolver.getFileContent('team', 'member', '/tmp/ledger-delete.txt', [
      {
        toolUseId: 'ledger-1',
        filePath: '/tmp/ledger-delete.txt',
        toolName: 'Bash',
        type: 'shell-snapshot',
        oldString: 'deleted\n',
        newString: '',
        replaceAll: false,
        timestamp: '2026-03-01T10:00:00.000Z',
        isError: false,
        ledger: {
          eventId: 'event-1',
          source: 'ledger-snapshot',
          confidence: 'high',
          originalFullContent: 'deleted\n',
          modifiedFullContent: null,
          beforeHash: 'hash',
          afterHash: null,
          operation: 'delete',
          beforeState: { exists: true, sha256: 'hash' },
          afterState: { exists: false },
        },
      },
    ]);

    expect(content.originalFullContent).toBe('deleted\n');
    expect(content.modifiedFullContent).toBe('');
    expect(content.contentSource).toBe('ledger-snapshot');
  });

  it('does not synthesize empty text for metadata-only ledger lifecycle states', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('current disk content that must not become ledger text');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');
    const resolver = new FileContentResolver({ findMemberLogPaths: vi.fn().mockResolvedValue([]) } as any);

    const content = await resolver.getFileContent('team', 'member', '/tmp/binary-create.bin', [
      {
        toolUseId: 'ledger-1',
        filePath: '/tmp/binary-create.bin',
        toolName: 'Bash',
        type: 'shell-snapshot',
        oldString: '',
        newString: '',
        replaceAll: false,
        timestamp: '2026-03-01T10:00:00.000Z',
        isError: false,
        ledger: {
          eventId: 'event-1',
          source: 'ledger-snapshot',
          confidence: 'high',
          originalFullContent: null,
          modifiedFullContent: null,
          beforeHash: null,
          afterHash: 'hash',
          operation: 'create',
          beforeState: { exists: false, unavailableReason: 'binary file' },
          afterState: { exists: true, sha256: 'hash', unavailableReason: 'binary file' },
        },
      },
    ]);

    expect(content.originalFullContent).toBeNull();
    expect(content.modifiedFullContent).toBeNull();
    expect(content.contentSource).toBe('unavailable');
    expect(readFile).not.toHaveBeenCalled();
  });

  it('reuses cached content only when disk bytes and snippets are unchanged', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('alpha');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);

    await resolver.resolveFileContent('team', 'member', '/tmp/cache-hit.txt', []);
    await resolver.resolveFileContent('team', 'member', '/tmp/cache-hit.txt', []);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(1);
  });

  it('misses cache when disk content changes even if snippets stay the same', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValueOnce('alpha').mockResolvedValueOnce('beta');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);

    await resolver.resolveFileContent('team', 'member', '/tmp/disk-change.txt', []);
    await resolver.resolveFileContent('team', 'member', '/tmp/disk-change.txt', []);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });

  it('misses cache when snippet fingerprint changes even if disk bytes stay the same', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('alpha');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);
    const firstSnippets: SnippetDiff[] = [];
    const secondSnippets: SnippetDiff[] = [
      {
        toolUseId: 't-edit',
        filePath: '/tmp/snippet-change.txt',
        toolName: 'Edit',
        type: 'edit',
        oldString: 'before',
        newString: 'after',
        replaceAll: false,
        timestamp: '2026-03-01T10:00:00.000Z',
        isError: false,
      },
    ];

    await resolver.resolveFileContent('team', 'member', '/tmp/snippet-change.txt', firstSnippets);
    await resolver.resolveFileContent('team', 'member', '/tmp/snippet-change.txt', secondSnippets);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });

  it('misses cache when snippet order changes even if snippet content stays the same', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('alpha');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);
    const firstSnippets: SnippetDiff[] = [
      {
        toolUseId: 't-1',
        filePath: '/tmp/snippet-order.txt',
        toolName: 'Edit',
        type: 'edit',
        oldString: 'a',
        newString: 'b',
        replaceAll: false,
        timestamp: '2026-03-01T10:00:00.000Z',
        isError: false,
      },
      {
        toolUseId: 't-2',
        filePath: '/tmp/snippet-order.txt',
        toolName: 'Edit',
        type: 'edit',
        oldString: 'c',
        newString: 'd',
        replaceAll: false,
        timestamp: '2026-03-01T10:01:00.000Z',
        isError: false,
      },
    ];
    const reversedSnippets = [...firstSnippets].reverse();

    await resolver.resolveFileContent('team', 'member', '/tmp/snippet-order.txt', firstSnippets);
    await resolver.resolveFileContent('team', 'member', '/tmp/snippet-order.txt', reversedSnippets);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });

  it('distinguishes missing-file fingerprints from empty-file fingerprints', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockRejectedValueOnce(new Error('ENOENT')).mockResolvedValueOnce('');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);

    const missing = await resolver.resolveFileContent(
      'team',
      'member',
      '/tmp/missing-vs-empty.txt',
      []
    );
    const empty = await resolver.resolveFileContent(
      'team',
      'member',
      '/tmp/missing-vs-empty.txt',
      []
    );

    expect(missing.source).toBe('unavailable');
    expect(empty.source).toBe('disk-current');
    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });

  it('uses the same provisional TTL for all content sources in this pass', async () => {
    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const resolver = new FileContentResolver({ findMemberLogPaths: vi.fn() } as any);
    const getCacheTtlForSource = (
      resolver as unknown as {
        getCacheTtlForSource: (source: string) => number;
      }
    ).getCacheTtlForSource.bind(resolver);

    expect(getCacheTtlForSource('file-history')).toBe(5_000);
    expect(getCacheTtlForSource('snippet-reconstruction')).toBe(5_000);
    expect(getCacheTtlForSource('git-fallback')).toBe(5_000);
    expect(getCacheTtlForSource('disk-current')).toBe(5_000);
    expect(getCacheTtlForSource('unavailable')).toBe(5_000);
  });

  it('expires provisional cache entries after the short TTL window', async () => {
    vi.useFakeTimers();

    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('alpha');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };

    const resolver = new FileContentResolver(logsFinder as any);

    await resolver.resolveFileContent('team', 'member', '/tmp/ttl-expiry.txt', []);
    vi.advanceTimersByTime(5_001);
    await resolver.resolveFileContent('team', 'member', '/tmp/ttl-expiry.txt', []);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });

  it('invalidates cached Windows content across slash and case path variants', async () => {
    const fsPromises = await import('fs/promises');
    const readFile = fsPromises.readFile as unknown as ReturnType<typeof vi.fn>;
    readFile.mockResolvedValue('same content');

    const { FileContentResolver } = await import('@main/services/team/FileContentResolver');

    const logsFinder = {
      findMemberLogPaths: vi.fn().mockResolvedValue([]),
    };
    const resolver = new FileContentResolver(logsFinder as any);

    await resolver.resolveFileContent('team', 'member', 'C:\\Repo\\SRC\\file.ts', []);
    resolver.invalidateFile('c:/repo/src/file.ts');
    await resolver.resolveFileContent('team', 'member', 'C:\\Repo\\SRC\\file.ts', []);

    expect(logsFinder.findMemberLogPaths).toHaveBeenCalledTimes(2);
  });
});
