import path from 'node:path';
import { Readable } from 'node:stream';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
  filterValidLines,
  readFilePathsFromStdin,
  readLinesFromStream,
  resolveAndDeduplicatePaths,
  type StdinDependencies,
} from '../../../src/core/file/fileStdin.js';
import { RepomixError } from '../../../src/shared/errorHandle.js';

describe('fileStdin', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('filterValidLines', () => {
    it('should filter out empty lines', () => {
      const lines = ['file1.txt', '', 'file2.txt', '   ', 'file3.txt'];
      const result = filterValidLines(lines);
      expect(result).toEqual(['file1.txt', 'file2.txt', 'file3.txt']);
    });

    it('should filter out comment lines starting with #', () => {
      const lines = ['file1.txt', '# This is a comment', 'file2.txt', '#another comment', 'file3.txt'];
      const result = filterValidLines(lines);
      expect(result).toEqual(['file1.txt', 'file2.txt', 'file3.txt']);
    });

    it('should trim whitespace from lines', () => {
      const lines = ['  file1.txt  ', '\tfile2.txt\t', ' file3.txt\n'];
      const result = filterValidLines(lines);
      expect(result).toEqual(['file1.txt', 'file2.txt', 'file3.txt']);
    });

    it('should return empty array when all lines are invalid', () => {
      const lines = ['', '# comment', '   ', '#another comment'];
      const result = filterValidLines(lines);
      expect(result).toEqual([]);
    });

    it('should handle mixed valid and invalid lines', () => {
      const lines = ['file1.txt', '', '# comment', '  file2.txt  ', '\t\t', '#another comment', 'file3.txt'];
      const result = filterValidLines(lines);
      expect(result).toEqual(['file1.txt', 'file2.txt', 'file3.txt']);
    });
  });

  describe('resolveAndDeduplicatePaths', () => {
    const cwd = path.resolve('/test/cwd');

    it('should resolve relative paths to absolute paths', () => {
      const lines = ['file1.txt', './file2.txt', '../file3.txt'];
      const result = resolveAndDeduplicatePaths(lines, cwd);

      const expected = [
        path.resolve(cwd, 'file1.txt'),
        path.resolve(cwd, 'file2.txt'),
        path.resolve(cwd, '../file3.txt'),
      ];
      expect(result).toEqual(expected);
    });

    it('should keep absolute paths as-is with normalization', () => {
      const absolutePath1 = path.resolve('/absolute/path/file1.txt');
      const absolutePath2 = path.resolve('/another/path/file2.txt');
      // Create a platform-specific complex path that should resolve to absolutePath2
      const complexPath = path.resolve('/another/./absolute/../path/file2.txt');
      const lines = [absolutePath1, complexPath];
      const result = resolveAndDeduplicatePaths(lines, cwd);

      expect(result).toEqual([absolutePath1, absolutePath2]);
    });

    it('should deduplicate identical paths', () => {
      const lines = ['file1.txt', './file1.txt', 'file1.txt'];
      const result = resolveAndDeduplicatePaths(lines, cwd);

      const expected = [path.resolve(cwd, 'file1.txt')];
      expect(result).toEqual(expected);
    });

    it('should handle mixed absolute and relative paths with duplicates', () => {
      const absolutePath1 = path.resolve(cwd, 'file1.txt');
      const absolutePath3 = path.resolve('/absolute/file3.txt');
      const lines = ['file1.txt', absolutePath1, './file2.txt', absolutePath3, 'file2.txt'];
      const result = resolveAndDeduplicatePaths(lines, cwd);

      const expected = [absolutePath1, path.resolve(cwd, 'file2.txt'), absolutePath3];
      expect(result).toEqual(expected);
    });

    it('should normalize paths with ./ and ../ segments', () => {
      const lines = ['./dir/../file1.txt', 'dir/./file2.txt', './dir/./sub/../file3.txt'];
      const result = resolveAndDeduplicatePaths(lines, cwd);

      const expected = [
        path.resolve(cwd, 'file1.txt'),
        path.resolve(cwd, 'dir/file2.txt'),
        path.resolve(cwd, 'dir/file3.txt'),
      ];
      expect(result).toEqual(expected);
    });
  });

  describe('readLinesFromStream', () => {
    it('should read lines from a readable stream', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          yield 'line1';
          yield 'line2';
          yield 'line3';
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStream = new Readable();

      const result = await readLinesFromStream(mockStream, mockCreateInterface);

      expect(mockCreateInterface).toHaveBeenCalledWith({ input: mockStream });
      expect(result).toEqual(['line1', 'line2', 'line3']);
    });

    it('should handle empty stream', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          // Empty generator
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStream = new Readable();

      const result = await readLinesFromStream(mockStream, mockCreateInterface);

      expect(result).toEqual([]);
    });

    it('should handle single line', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          yield 'single line';
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStream = new Readable();

      const result = await readLinesFromStream(mockStream, mockCreateInterface);

      expect(result).toEqual(['single line']);
    });
  });

  describe('readFilePathsFromStdin', () => {
    const cwd = path.resolve('/test/cwd');

    it('should throw error when stdin is TTY', async () => {
      const mockStdin = { isTTY: true } as NodeJS.ReadableStream & { isTTY?: boolean };
      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: vi.fn(),
      };

      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(RepomixError);
      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(
        'No data provided via stdin. Please pipe file paths to repomix when using --stdin flag.',
      );
    });

    it('should throw error when no valid file paths found', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          yield '';
          yield '# comment';
          yield '   ';
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStdin = { isTTY: false } as NodeJS.ReadableStream & { isTTY?: boolean };

      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: mockCreateInterface,
      };

      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(RepomixError);
      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow('No valid file paths found in stdin input.');
    });

    it('should successfully read and process file paths from stdin', async () => {
      const absoluteFile3 = path.resolve('/absolute/file3.txt');
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          yield 'file1.txt';
          yield '# comment';
          yield './file2.txt';
          yield '';
          yield absoluteFile3;
          yield 'file1.txt'; // duplicate
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStdin = { isTTY: false } as NodeJS.ReadableStream & { isTTY?: boolean };

      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: mockCreateInterface,
      };

      const result = await readFilePathsFromStdin(cwd, deps);

      expect(mockCreateInterface).toHaveBeenCalledWith({ input: mockStdin });
      expect(result).toEqual({
        filePaths: [path.resolve(cwd, 'file1.txt'), path.resolve(cwd, 'file2.txt'), absoluteFile3],
        emptyDirPaths: [],
      });
    });

    it('should handle complex path normalization', async () => {
      const absoluteFile3 = path.resolve('/absolute/file3.txt');
      // Create a complex path that resolves to the same as absoluteFile3
      const complexAbsolutePath = path.resolve('/absolute/./path/../file3.txt');
      const mockInterface = {
        [Symbol.asyncIterator]: async function* () {
          yield './dir/../file1.txt';
          yield 'dir/./file2.txt';
          yield complexAbsolutePath;
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStdin = { isTTY: false } as NodeJS.ReadableStream & { isTTY?: boolean };

      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: mockCreateInterface,
      };

      const result = await readFilePathsFromStdin(cwd, deps);

      expect(result.filePaths).toEqual([
        path.resolve(cwd, 'file1.txt'),
        path.resolve(cwd, 'dir', 'file2.txt'),
        absoluteFile3,
      ]);
    });

    it('should wrap generic errors in RepomixError', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* (): AsyncGenerator<string, void, unknown> {
          // This generator needs to throw an error for testing, but must yield first to satisfy require-yield
          yield 'dummy';
          throw new Error('Generic error');
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStdin = { isTTY: false } as NodeJS.ReadableStream & { isTTY?: boolean };

      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: mockCreateInterface,
      };

      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(RepomixError);
      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(
        'Failed to read file paths from stdin: Generic error',
      );
    });

    it('should propagate RepomixError without wrapping', async () => {
      const mockStdin = { isTTY: true } as NodeJS.ReadableStream & { isTTY?: boolean };
      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: vi.fn(),
      };

      const error = await readFilePathsFromStdin(cwd, deps).catch((e) => e);
      expect(error).toBeInstanceOf(RepomixError);
      expect(error.message).toBe(
        'No data provided via stdin. Please pipe file paths to repomix when using --stdin flag.',
      );
    });

    it('should handle unknown error types', async () => {
      const mockInterface = {
        [Symbol.asyncIterator]: async function* (): AsyncGenerator<string, void, unknown> {
          // This generator needs to throw an error for testing, but must yield first to satisfy require-yield
          yield 'dummy';
          throw 'string error';
        },
      };

      const mockCreateInterface = vi.fn().mockReturnValue(mockInterface);
      const mockStdin = { isTTY: false } as NodeJS.ReadableStream & { isTTY?: boolean };

      const deps: StdinDependencies = {
        stdin: mockStdin,
        createReadlineInterface: mockCreateInterface,
      };

      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(RepomixError);
      await expect(readFilePathsFromStdin(cwd, deps)).rejects.toThrow(
        'An unexpected error occurred while reading from stdin.',
      );
    });
  });
});
