// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { ChangelogIndexItem } from '@/types/changelog';

import { ChangelogService } from './index';

// Mock external dependencies
vi.mock('dayjs', () => ({
  default: (date: string) => ({
    format: vi.fn().mockReturnValue(date),
  }),
}));

vi.mock('gray-matter', () => ({
  default: vi.fn().mockImplementation((text) => ({
    data: { date: '2023-01-01' },
    content: text,
  })),
}));

vi.mock('markdown-to-txt', () => ({
  markdownToTxt: vi.fn().mockImplementation((text) => text),
}));

vi.mock('semver', async (importOriginal) => {
  const actual: any = await importOriginal();
  return {
    ...actual,
    rcompare: vi.fn().mockImplementation((a, b) => b.localeCompare(a)),
    lt: vi.fn().mockImplementation((a, b) => a < b),
    gt: vi.fn().mockImplementation((a, b) => a > b),
    parse: vi.fn().mockImplementation((v) => ({ toString: () => v })),
  };
});

vi.mock('url-join', () => ({
  default: vi.fn((...args) => args.join('/')),
}));

// 模拟 process.env
const originalEnv = process.env;

beforeEach(() => {
  vi.resetModules();
  process.env = { ...originalEnv };
});

afterEach(() => {
  process.env = originalEnv;
});

describe('ChangelogService', () => {
  let service: ChangelogService;

  beforeEach(() => {
    service = new ChangelogService();
    // Mock fetch globally
    global.fetch = vi.fn();
  });

  describe('getLatestChangelogId', () => {
    it('should return the id of the first changelog item', async () => {
      const mockIndex = [{ id: 'latest' }, { id: 'older' }];
      vi.spyOn(service, 'getChangelogIndex').mockResolvedValue(mockIndex as ChangelogIndexItem[]);

      const result = await service.getLatestChangelogId();
      expect(result).toBe('latest');
    });

    it('should return undefined if the index is empty', async () => {
      vi.spyOn(service, 'getChangelogIndex').mockResolvedValue([]);

      const result = await service.getLatestChangelogId();
      expect(result).toBeUndefined();
    });
  });

  describe('getChangelogIndex', () => {
    it('should fetch and merge changelog data', async () => {
      const mockResponse = {
        json: vi.fn().mockResolvedValue({
          cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
          community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],
        }),
      };
      (global.fetch as any).mockResolvedValue(mockResponse);

      const result = await service.getChangelogIndex();
      expect(result).toHaveLength(2);
      expect(result[0].id).toBe('community1');
      expect(result[1].id).toBe('cloud1');
    });

    it('should handle fetch errors', async () => {
      (global.fetch as any).mockRejectedValue(
        new Error('Fetch failed', { cause: { code: 'Timeout' } }),
      );

      const result = await service.getChangelogIndex();
      expect(result).toEqual([]);
    });

    it('should return only community items when config type is community', async () => {
      service.config.type = 'community';
      const mockResponse = {
        json: vi.fn().mockResolvedValue({
          cloud: [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }],
          community: [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }],
        }),
      };
      (global.fetch as any).mockResolvedValue(mockResponse);

      const result = await service.getChangelogIndex();
      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('community1');
    });
  });

  describe('getIndexItemById', () => {
    it('should return the correct item by id', async () => {
      const mockIndex = [
        { id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'] },
        { id: 'item2', date: '2023-01-02', versionRange: ['1.1.0'] },
      ];
      vi.spyOn(service, 'getChangelogIndex').mockResolvedValue(mockIndex as ChangelogIndexItem[]);

      const result = await service.getIndexItemById('item2');
      expect(result).toEqual({ id: 'item2', date: '2023-01-02', versionRange: ['1.1.0'] });
    });

    it('should return undefined for non-existent id', async () => {
      vi.spyOn(service, 'getChangelogIndex').mockResolvedValue([]);

      const result = await service.getIndexItemById('nonexistent');
      expect(result).toBeUndefined();
    });
  });

  describe('getPostById', () => {
    it('should fetch and parse post content', async () => {
      vi.spyOn(service, 'getIndexItemById').mockResolvedValue({
        id: 'post1',
        date: '2023-01-01',
        versionRange: ['1.0.0'],
      } as ChangelogIndexItem);

      const mockResponse = {
        text: vi.fn().mockResolvedValue('# Post Title\nPost content'),
      };
      (global.fetch as any).mockResolvedValue(mockResponse);

      const result = await service.getPostById('post1');
      expect(result).toMatchObject({
        content: 'Post content',
        date: expect.any(String), // 改为期望字符串而不是 Date 对象
        description: 'Post content',
        image: undefined,
        rawTitle: 'Post Title',
        tags: ['changelog'],
        title: 'Post Title',
      });

      // 额外检查日期格式
      expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
    });

    it('should handle fetch errors', async () => {
      vi.spyOn(service, 'getIndexItemById').mockResolvedValue({} as ChangelogIndexItem);
      (global.fetch as any).mockRejectedValue(new Error('Fetch failed'));

      const result = await service.getPostById('error');
      expect(result).toBe(false);
    });

    it('should use the correct locale for fetching content', async () => {
      vi.spyOn(service, 'getIndexItemById').mockResolvedValue({
        id: 'post1',
        date: '2023-01-01',
        versionRange: ['1.0.0'],
      } as ChangelogIndexItem);

      const mockResponse = {
        text: vi.fn().mockResolvedValue('# Chinese Title\n中文内容'),
      };
      (global.fetch as any).mockResolvedValue(mockResponse);

      const result = await service.getPostById('post1', { locale: 'zh-CN' });
      expect(result).toEqual({
        content: '中文内容',
        date: '2023-01-01',
        description: '中文内容',
        image: undefined,
        rawTitle: 'Chinese Title',
        tags: ['changelog'],
        title: 'Chinese Title',
      });
    });
  });

  describe('private methods', () => {
    describe('mergeChangelogs', () => {
      it('should merge and sort changelogs correctly', () => {
        const cloud = [{ id: 'cloud1', date: '2023-01-01', versionRange: ['1.0.0'] }];
        const community = [{ id: 'community1', date: '2023-01-02', versionRange: ['1.1.0'] }];

        // @ts-ignore - accessing private method for testing
        const result = service.mergeChangelogs(cloud, community);
        expect(result).toHaveLength(2);
        expect(result[0].id).toBe('community1');
        expect(result[1].id).toBe('cloud1');
      });

      it('should override community items with cloud items when ids match', () => {
        const cloud = [{ id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'], type: 'cloud' }];
        const community = [
          { id: 'item1', date: '2023-01-01', versionRange: ['1.0.0'], type: 'community' },
        ];

        // @ts-ignore - accessing private method for testing
        const result = service.mergeChangelogs(cloud, community);
        expect(result).toHaveLength(1);
        // @ts-ignore
        expect(result[0].type).toBe('cloud');
      });
    });

    describe('formatVersionRange', () => {
      it('should format version range correctly', () => {
        // @ts-ignore - accessing private method for testing
        const result = service.formatVersionRange(['1.0.0', '1.1.0']);
        expect(result).toEqual(['1.0.0', '1.1.0']);
      });

      it('should return single version as is', () => {
        // @ts-ignore - accessing private method for testing
        const result = service.formatVersionRange(['1.0.0']);
        expect(result).toEqual(['1.0.0']);
      });
    });

    describe('genUrl', () => {
      it('should generate correct URL', () => {
        // @ts-ignore - accessing private method for testing
        const result = service.genUrl('test/path');
        expect(result).toBe('https://raw.githubusercontent.com/lobehub/lobe-chat/main/test/path');
      });
    });

    describe('extractHttpsLinks', () => {
      it('should extract HTTPS links from text', () => {
        const text = 'Text with https://example.com and https://test.com/image.jpg links';
        // @ts-ignore - accessing private method for testing
        const result = service.extractHttpsLinks(text);
        expect(result).toEqual(['https://example.com', 'https://test.com/image.jpg']);
      });
    });

    describe('cdnInit', () => {
      it('should initialize CDN URLs if docCdnPrefix is set', async () => {
        // 设置环境变量
        process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';

        // 重新导入模块以确保环境变量生效
        const { ChangelogService } = await import('./index');
        const service = new ChangelogService();

        const mockData = { 'https://example.com/image.jpg': 'image-hash.jpg' };
        const mockResponse = {
          json: vi.fn().mockResolvedValue(mockData),
        };
        global.fetch = vi.fn().mockResolvedValue(mockResponse);

        // @ts-ignore - accessing private method for testing
        await service.cdnInit();

        expect(service.cdnUrls).toEqual(mockData);
      });
    });

    describe('replaceCdnUrl', () => {
      it('should replace URL with CDN URL if available', async () => {
        // 设置环境变量
        process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';

        // 重新导入模块以确保环境变量生效
        const { ChangelogService } = await import('./index');
        const service = new ChangelogService();

        service.cdnUrls = { 'https://example.com/image.jpg': 'image-hash.jpg' };

        // @ts-ignore - accessing private method for testing
        const result = service.replaceCdnUrl('https://example.com/image.jpg');

        expect(result).toBe('https://cdn.example.com/image-hash.jpg');
      });

      it('should return original URL if CDN URL is not available', () => {
        const originalDocCdnPrefix = process.env.DOC_S3_PUBLIC_DOMAIN;
        process.env.DOC_S3_PUBLIC_DOMAIN = 'https://cdn.example.com';
        service.cdnUrls = {};

        // @ts-ignore - accessing private method for testing
        const result = service.replaceCdnUrl('https://example.com/image.jpg');
        expect(result).toBe('https://example.com/image.jpg');

        // Restore original value
        process.env.DOC_S3_PUBLIC_DOMAIN = originalDocCdnPrefix;
      });
    });
  });
});
