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

// Mock child_process for project.ts
vi.mock('child_process', () => ({
  exec: vi.fn((_cmd, _opts, callback) => {
    callback(null, { stdout: 'abc123def456\n' });
  }),
}));

// Mock util for promisify
vi.mock('util', async () => {
  const actual = await vi.importActual('util');
  return {
    ...actual,
    promisify: vi.fn(() => vi.fn().mockResolvedValue({ stdout: 'abc123def456\n' })),
  };
});

// Mock fs/promises
vi.mock('fs/promises', () => ({
  mkdir: vi.fn().mockResolvedValue(undefined),
  writeFile: vi.fn().mockResolvedValue(undefined),
  readFile: vi.fn().mockResolvedValue('{}'),
  readdir: vi.fn().mockResolvedValue([]),
  unlink: vi.fn().mockResolvedValue(undefined),
  rm: vi.fn().mockResolvedValue(undefined),
  access: vi.fn().mockResolvedValue(undefined),
  stat: vi.fn().mockResolvedValue({ isDirectory: () => false }),
}));

// Mock os
vi.mock('os', () => ({
  homedir: vi.fn(() => '/home/testuser'),
}));

// Mock Lock to avoid actual locking
vi.mock('../../../src/utils/lock.js', () => ({
  Lock: {
    read: vi.fn().mockResolvedValue({ [Symbol.dispose]: vi.fn() }),
    write: vi.fn().mockResolvedValue({ [Symbol.dispose]: vi.fn() }),
  },
}));

// Mock migration to avoid running actual migrations
vi.mock('../../../src/session/migration.js', () => ({
  runMigrations: vi.fn().mockResolvedValue(undefined),
}));

// Mock project to return predictable project ID
vi.mock('../../../src/session/project.js', () => ({
  getProjectId: vi.fn().mockResolvedValue('test-project-id'),
  isGitRepository: vi.fn().mockResolvedValue(true),
}));

import { SessionStorage } from '../../../src/session/storage.js';
import * as fs from 'fs/promises';

describe('SessionStorage - 会话存储', () => {
  let storage: SessionStorage;

  beforeEach(() => {
    vi.clearAllMocks();
    storage = new SessionStorage('/test/storage');
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('构造函数', () => {
    it('使用提供的存储目录', () => {
      const s = new SessionStorage('/custom/path');
      expect(s.getStorageDir()).toBe('/custom/path');
    });

    it('默认使用 XDG 规范路径', () => {
      const originalEnv = process.env.XDG_DATA_HOME;
      process.env.XDG_DATA_HOME = '/xdg/data';

      const s = new SessionStorage();
      expect(s.getStorageDir()).toBe('/xdg/data/ai-assist');

      process.env.XDG_DATA_HOME = originalEnv;
    });

    it('无 XDG 环境变量使用 home 目录', () => {
      const originalEnv = process.env.XDG_DATA_HOME;
      delete process.env.XDG_DATA_HOME;

      const s = new SessionStorage();
      expect(s.getStorageDir()).toContain('.local/share/ai-assist');

      process.env.XDG_DATA_HOME = originalEnv;
    });
  });

  describe('generateSessionId - 生成会话 ID', () => {
    it('生成包含日期的会话 ID', () => {
      const id = storage.generateSessionId();

      // 格式: YYYY-MM-DD_xxxxxx
      expect(id).toMatch(/^\d{4}-\d{2}-\d{2}_[a-z0-9]{6}$/);
    });

    it('生成唯一的会话 ID', () => {
      const ids = new Set();
      for (let i = 0; i < 100; i++) {
        ids.add(storage.generateSessionId());
      }
      expect(ids.size).toBe(100);
    });
  });

  describe('ensureDir - 确保目录存在', () => {
    it('创建必要的目录', async () => {
      await storage.ensureDir();

      // 应该创建 project, session, message 目录
      expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('project'), { recursive: true });
      expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('session'), { recursive: true });
      expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('message'), { recursive: true });
    });
  });

  describe('getOrCreateProject - 获取或创建项目', () => {
    it('返回项目元数据', async () => {
      vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('ENOENT'));

      const project = await storage.getOrCreateProject('/test/workdir');

      expect(project).toMatchObject({
        id: 'test-project-id',
        workdir: '/test/workdir',
        isGitRepo: true,
      });
    });

    it('已存在的项目直接返回', async () => {
      const existingProject = {
        id: 'existing-project',
        workdir: '/existing',
        createdAt: '2024-01-01T00:00:00Z',
        isGitRepo: false,
      };
      vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(existingProject));

      const project = await storage.getOrCreateProject('/existing');

      expect(project).toEqual(existingProject);
    });
  });

  describe('saveSessionMetadata - 保存会话元数据', () => {
    it('保存会话元数据到文件', async () => {
      const session = {
        id: 'test-session',
        projectId: 'test-project',
        workdir: '/test',
        messages: [{ role: 'user', content: 'hello' }],
        createdAt: '2024-01-01T00:00:00Z',
        updatedAt: '2024-01-01T00:00:00Z',
        discoveredTools: [],
        todos: [],
      };

      await storage.saveSessionMetadata(session as any);

      expect(fs.writeFile).toHaveBeenCalledWith(
        expect.stringContaining('test-session.json'),
        expect.any(String),
        'utf-8'
      );
    });
  });

  describe('loadSession - 加载完整会话', () => {
    it('成功加载会话（元数据 + 消息）', async () => {
      const metadata = {
        id: 'session-123',
        projectId: 'test-project',
        workdir: '/test',
        messageCount: 2,
        createdAt: '2024-01-01T00:00:00Z',
        updatedAt: '2024-01-01T00:00:00Z',
        discoveredTools: [],
        todos: [],
      };

      vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(metadata));
      vi.mocked(fs.readdir).mockResolvedValueOnce(['0001.json', '0002.json'] as any);
      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(JSON.stringify({ index: 1, role: 'user', content: 'hello' }))
        .mockResolvedValueOnce(JSON.stringify({ index: 2, role: 'assistant', content: 'hi' }));

      const session = await storage.loadSession('test-project', 'session-123');

      expect(session).toMatchObject({
        id: 'session-123',
        projectId: 'test-project',
      });
      expect(session?.messages).toHaveLength(2);
    });

    it('会话不存在返回 null', async () => {
      vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('ENOENT'));

      const session = await storage.loadSession('test-project', 'nonexistent');

      expect(session).toBeNull();
    });
  });

  describe('appendMessage - 追加消息', () => {
    it('写入消息文件', async () => {
      await storage.appendMessage('session-123', { role: 'user', content: 'hello' } as any, 1);

      expect(fs.writeFile).toHaveBeenCalled();
      const [filePath, content] = vi.mocked(fs.writeFile).mock.calls[0];
      expect(filePath).toContain('0001.json');
      const parsed = JSON.parse(content as string);
      expect(parsed.role).toBe('user');
      expect(parsed.content).toBe('hello');
      expect(parsed.index).toBe(1);
    });
  });

  describe('loadMessages - 加载消息', () => {
    it('按顺序加载所有消息', async () => {
      vi.mocked(fs.readdir).mockResolvedValueOnce(['0001.json', '0002.json'] as any);
      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(JSON.stringify({ index: 1, role: 'user', content: 'first' }))
        .mockResolvedValueOnce(JSON.stringify({ index: 2, role: 'assistant', content: 'second' }));

      const messages = await storage.loadMessages('session-123');

      expect(messages).toHaveLength(2);
      expect(messages[0].content).toBe('first');
      expect(messages[1].content).toBe('second');
    });

    it('目录不存在返回空数组', async () => {
      vi.mocked(fs.readdir).mockRejectedValueOnce(new Error('ENOENT'));

      const messages = await storage.loadMessages('nonexistent');

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

  describe('deleteSession - 删除会话', () => {
    it('成功删除返回 true', async () => {
      const result = await storage.deleteSession('test-project', 'session-123');

      expect(result).toBe(true);
      expect(fs.rm).toHaveBeenCalled(); // 删除消息目录
      expect(fs.unlink).toHaveBeenCalled(); // 删除元数据文件
    });

    it('删除失败返回 false', async () => {
      vi.mocked(fs.rm).mockRejectedValueOnce(new Error('ENOENT'));
      vi.mocked(fs.unlink).mockRejectedValueOnce(new Error('ENOENT'));

      const result = await storage.deleteSession('test-project', 'nonexistent');

      expect(result).toBe(false);
    });
  });

  describe('setCurrentSession / getCurrentSessionId', () => {
    it('设置和获取当前会话 ID', async () => {
      await storage.setCurrentSession('session-123');

      expect(fs.writeFile).toHaveBeenCalled();
      const calls = vi.mocked(fs.writeFile).mock.calls;
      const lastCall = calls[calls.length - 1];
      expect(lastCall[0]).toContain('current-session.json');
      const parsed = JSON.parse(lastCall[1] as string);
      expect(parsed.sessionId).toBe('session-123');
    });

    it('获取当前会话 ID', async () => {
      vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify({ sessionId: 'session-123' }));

      const sessionId = await storage.getCurrentSessionId();

      expect(sessionId).toBe('session-123');
    });

    it('无当前会话返回 null', async () => {
      vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('ENOENT'));

      const sessionId = await storage.getCurrentSessionId();

      expect(sessionId).toBeNull();
    });
  });

  describe('listSessionsByProject - 列出项目会话', () => {
    it('返回会话摘要列表', async () => {
      vi.mocked(fs.readdir).mockResolvedValueOnce(['session1.json', 'session2.json'] as any);

      const metadata1 = {
        id: 'session1',
        projectId: 'test-project',
        workdir: '/test1',
        messageCount: 1,
        createdAt: '2024-01-01T00:00:00Z',
        updatedAt: '2024-01-02T00:00:00Z',
        discoveredTools: [],
        todos: [],
      };
      const metadata2 = {
        id: 'session2',
        projectId: 'test-project',
        workdir: '/test2',
        messageCount: 0,
        createdAt: '2024-01-03T00:00:00Z',
        updatedAt: '2024-01-04T00:00:00Z',
        discoveredTools: [],
        todos: [],
      };

      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(JSON.stringify(metadata1))
        .mockResolvedValueOnce(JSON.stringify(metadata2));

      const sessions = await storage.listSessionsByProject('test-project');

      expect(sessions).toHaveLength(2);
      // 按更新时间降序
      expect(sessions[0].id).toBe('session2');
      expect(sessions[1].id).toBe('session1');
    });
  });

  describe('cleanupOldSessions - 清理旧会话', () => {
    it('删除超出保留数量的会话', async () => {
      // Mock listAllSessions
      vi.mocked(fs.readdir)
        .mockResolvedValueOnce(['project1'] as any) // listAllSessions - 项目目录
        .mockResolvedValueOnce(['session1.json', 'session2.json', 'session3.json'] as any); // listSessionsByProject

      const sessions = [
        { id: 'session3', messageCount: 0, updatedAt: '2024-01-03', projectId: 'project1' },
        { id: 'session2', messageCount: 0, updatedAt: '2024-01-02', projectId: 'project1' },
        { id: 'session1', messageCount: 0, updatedAt: '2024-01-01', projectId: 'project1' },
      ];

      vi.mocked(fs.readFile)
        .mockResolvedValueOnce(
          JSON.stringify({ ...sessions[0], workdir: '/', createdAt: '2024-01-01', discoveredTools: [], todos: [] })
        )
        .mockResolvedValueOnce(
          JSON.stringify({ ...sessions[1], workdir: '/', createdAt: '2024-01-01', discoveredTools: [], todos: [] })
        )
        .mockResolvedValueOnce(
          JSON.stringify({ ...sessions[2], workdir: '/', createdAt: '2024-01-01', discoveredTools: [], todos: [] })
        );

      // Mock for deleteSession lookup
      vi.mocked(fs.readdir).mockResolvedValueOnce(['project1'] as any);
      vi.mocked(fs.access).mockResolvedValueOnce(undefined);

      const deletedCount = await storage.cleanupOldSessions(2);

      expect(deletedCount).toBe(1);
    });

    it('会话数量不超过保留数量不删除', async () => {
      vi.mocked(fs.readdir)
        .mockResolvedValueOnce(['project1'] as any)
        .mockResolvedValueOnce(['session1.json'] as any);

      vi.mocked(fs.readFile).mockResolvedValueOnce(
        JSON.stringify({
          id: 'session1',
          projectId: 'project1',
          messageCount: 0,
          workdir: '/',
          createdAt: '2024-01-01',
          updatedAt: '2024-01-01',
          discoveredTools: [],
          todos: [],
        })
      );

      const deletedCount = await storage.cleanupOldSessions(5);

      expect(deletedCount).toBe(0);
    });
  });
});
