/**
 * GiteaClientService Unit Tests
 *
 * 覆盖 03-architecture §3 + §6 + 07-api §8.4 的 Gitea 集成契约：
 * - createRepo: 201 成功 / 409 已存在 / 403 scope 不足 / 404 org 不存在
 * - getRepo: 200 存在 / 404 不存在 / 错误透传
 * - issuePushCredential: token 缺失 → gitea_token_missing
 * - parseError: 多种 HTTP 错误的结构化映射
 *
 * 使用 jest.fn 替换 global.fetch，纯单元测试不打外部网络。
 */

import { ConfigService } from '@nestjs/config';
import { GiteaClientService } from '@/modules/internal-app-platform/services/gitea-client.service';

function mockFetchOnce(status: number, body: unknown) {
  const isString = typeof body === 'string';
  (global.fetch as jest.Mock).mockResolvedValueOnce({
    status,
    json: async () => (isString ? JSON.parse(body) : body),
    text: async () => (isString ? body : JSON.stringify(body)),
  });
}

function makeConfig(token: string | undefined): ConfigService {
  return {
    get: (key: string) => {
      if (key === 'INTERNAL_APP_GITEA_API_TOKEN') return token;
      if (key === 'INTERNAL_APP_GITEA_BASE_URL') return 'http://test.gitea';
      return undefined;
    },
  } as ConfigService;
}

describe('GiteaClientService', () => {
  beforeEach(() => {
    global.fetch = jest.fn() as unknown as typeof fetch;
  });
  afterEach(() => {
    jest.resetAllMocks();
  });

  describe('未配 token', () => {
    it('createRepo 立即返回 gitea_token_missing', async () => {
      const svc = new GiteaClientService(makeConfig(undefined));
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error.code).toBe('gitea_token_missing');
      expect(global.fetch).not.toHaveBeenCalled();
    });

    it('issuePushCredential 立即返回 gitea_token_missing', () => {
      const svc = new GiteaClientService(makeConfig(undefined));
      const result = svc.issuePushCredential();
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error.code).toBe('gitea_token_missing');
    });
  });

  describe('createRepo', () => {
    let svc: GiteaClientService;
    beforeEach(() => {
      svc = new GiteaClientService(makeConfig('test-token'));
    });

    it('201 成功 → 返回 repoSummary', async () => {
      mockFetchOnce(201, {
        id: 42,
        full_name: 'FFAIApps/zhang-san-birthday-reminder',
        clone_url: 'http://test.gitea/FFAIApps/zhang-san-birthday-reminder.git',
        ssh_url: 'ssh://test.gitea/FFAIApps/zhang-san-birthday-reminder.git',
      });
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.repo.id).toBe(42);
        expect(result.repo.fullName).toBe('FFAIApps/zhang-san-birthday-reminder');
      }

      // 校验请求形状
      const callArgs = (global.fetch as jest.Mock).mock.calls[0];
      expect(callArgs[0]).toBe('http://test.gitea/api/v1/orgs/FFAIApps/repos');
      expect(callArgs[1].method).toBe('POST');
      expect(callArgs[1].headers.Authorization).toBe('token test-token');
      const sent = JSON.parse(callArgs[1].body as string);
      expect(sent.name).toBe('zhang-san-birthday-reminder');
      expect(sent.auto_init).toBe(false);
      expect(sent.default_branch).toBe('main');
    });

    it('409 → gitea_repo_already_exists', async () => {
      mockFetchOnce(409, '{"message":"repository already exists"}');
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe('gitea_repo_already_exists');
        if (result.error.code === 'gitea_repo_already_exists') {
          expect(result.error.fullName).toBe('FFAIApps/zhang-san-birthday-reminder');
        }
      }
    });

    it('403 with required scope → gitea_token_insufficient_scope + 解析 required', async () => {
      mockFetchOnce(
        403,
        '{"message":"token does not have at least one of required scope(s), required=[write:organization]"}',
      );
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe('gitea_token_insufficient_scope');
        if (result.error.code === 'gitea_token_insufficient_scope') {
          expect(result.error.required).toContain('write:organization');
        }
      }
    });

    it('404 → gitea_org_not_found', async () => {
      mockFetchOnce(404, '{"message":"organization does not exist"}');
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe('gitea_org_not_found');
        if (result.error.code === 'gitea_org_not_found') {
          expect(result.error.org).toBe('FFAIApps');
        }
      }
    });

    it('500 → gitea_unknown + 保留 body 摘要', async () => {
      mockFetchOnce(500, '{"message":"internal server error"}');
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe('gitea_unknown');
        if (result.error.code === 'gitea_unknown') {
          expect(result.error.status).toBe(500);
        }
      }
    });

    it('fetch 抛错 → gitea_unreachable + cause', async () => {
      (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('ECONNREFUSED'));
      const result = await svc.createRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe('gitea_unreachable');
        if (result.error.code === 'gitea_unreachable') {
          expect(result.error.cause).toBe('ECONNREFUSED');
        }
      }
    });
  });

  describe('getRepo', () => {
    let svc: GiteaClientService;
    beforeEach(() => {
      svc = new GiteaClientService(makeConfig('test-token'));
    });

    it('200 → exists=true + repoSummary', async () => {
      mockFetchOnce(200, {
        id: 7,
        full_name: 'FFAIApps/zhang-san-foo',
        clone_url: 'http://test.gitea/FFAIApps/zhang-san-foo.git',
        ssh_url: 'ssh://test.gitea/FFAIApps/zhang-san-foo.git',
      });
      const result = await svc.getRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'foo',
      });
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.exists).toBe(true);
        expect(result.repo?.id).toBe(7);
      }
    });

    it('404 → exists=false（不当作错误）', async () => {
      mockFetchOnce(404, '{"message":"not found"}');
      const result = await svc.getRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'foo',
      });
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.exists).toBe(false);
        expect(result.repo).toBeUndefined();
      }
    });

    it('403 scope 不足 → 错误透传', async () => {
      mockFetchOnce(
        403,
        '{"message":"token does not have at least one of required scope(s), required=[read:repository]"}',
      );
      const result = await svc.getRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'foo',
      });
      expect(result.ok).toBe(false);
    });
  });

  describe('archiveRepo', () => {
    let svc: GiteaClientService;
    beforeEach(() => {
      svc = new GiteaClientService(makeConfig('test-token'));
    });

    it('200 成功 → ok + alreadyArchived=true if response.archived=true', async () => {
      (global.fetch as jest.Mock).mockResolvedValue({
        status: 200,
        json: async () => ({ archived: true }),
      });
      const result = await svc.archiveRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'birthday-reminder',
      });
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.alreadyArchived).toBe(true);
      const call = (global.fetch as jest.Mock).mock.calls[0];
      expect(call[1].method).toBe('PATCH');
      expect(call[1].body).toContain('"archived":true');
    });

    it('404 仓库不存在 → 幂等视为成功（alreadyArchived=false）', async () => {
      (global.fetch as jest.Mock).mockResolvedValue({
        status: 404,
        text: async () => '',
      });
      const result = await svc.archiveRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'no-such-app',
      });
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.alreadyArchived).toBe(false);
    });

    it('未配 token → 立即 gitea_token_missing 不发请求', async () => {
      const noTokenSvc = new GiteaClientService(makeConfig(undefined));
      const result = await noTokenSvc.archiveRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'foo',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error.code).toBe('gitea_token_missing');
      expect(global.fetch).not.toHaveBeenCalled();
    });

    it('网络错误 → gitea_unreachable', async () => {
      (global.fetch as jest.Mock).mockRejectedValue(new Error('ECONNREFUSED'));
      const result = await svc.archiveRepo({
        employeeSlug: 'zhang-san',
        appSlug: 'foo',
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.error.code).toBe('gitea_unreachable');
    });
  });

  describe('issuePushCredential', () => {
    it('Phase 0 实现：返回 long-term token + 5min expiresAt（语义提示）', () => {
      const svc = new GiteaClientService(makeConfig('test-token'));
      const result = svc.issuePushCredential();
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.credential.token).toBe('test-token');
        expect(result.credential.isEphemeral).toBe(false);
        const ms = result.credential.expiresAt.getTime() - Date.now();
        expect(ms).toBeGreaterThan(4 * 60 * 1000);
        expect(ms).toBeLessThan(6 * 60 * 1000);
      }
    });
  });
});
