import { UserAiController } from './user-ai.controller';
import { LearningAnalysisExecutionRouter } from './learning-analysis-execution-router';
import { QuotaGuardService } from '../membership/quota-guard.service';
import type {
  CreateAnalysisJobDto,
  CreateCredentialDto,
  SaveLearningProfileDto,
  UpdateAiSettingsDto,
  UpdateCredentialDto,
} from './user-ai.dto';

describe('UserAiController', () => {
  let controller: UserAiController;
  let mockService: any;
  let mockQuizRouter: any;
  let mockLearningAnalysisRouter: LearningAnalysisExecutionRouter;
  let mockQuotaGuard: QuotaGuardService;

  beforeEach(() => {
    mockService = {
      getProfile: jest.fn(),
      saveProfile: jest.fn(),
      getSettings: jest.fn(),
      updateSettings: jest.fn(),
      listCredentials: jest.fn(),
      createCredential: jest.fn(),
      updateCredential: jest.fn(),
      deleteCredential: jest.fn(),
      testCredential: jest.fn(),
      createAnalysisJob: jest.fn(),
      cancelJob: jest.fn(),
      getJob: jest.fn(),
      listJobs: jest.fn(),
      publishQuiz: jest.fn(),
      publishFlashcard: jest.fn(),
      getAnalysis: jest.fn(),
      listAnalyses: jest.fn(),
      listRecommendations: jest.fn(),
      listWeakPoints: jest.fn(),
      getQuiz: jest.fn(),
      getQuizQuestions: jest.fn(),
      listQuizzes: jest.fn(),
      triggerReanalysis: jest.fn(),
      listNotifications: jest.fn(),
      submitArtifactFeedback: jest.fn(),
      submitFeedback: jest.fn(),
      getFlashcard: jest.fn(),
      listFlashcards: jest.fn(),
    };
    mockQuizRouter = {
      generateQuiz: jest.fn(),
    };
    mockLearningAnalysisRouter = new LearningAnalysisExecutionRouter(
      { isEnabled: jest.fn() } as never,
      { build: jest.fn() } as never,
      { createJob: jest.fn() } as never,
      { createAnalysisJob: jest.fn() } as never,
    );
    jest.spyOn(mockLearningAnalysisRouter, 'analyze').mockResolvedValue({
      jobId: 'analysis-1',
      status: 'queued',
      engineMode: 'unified',
      lifecycleStatus: 'queued',
    });
    mockQuotaGuard = new QuotaGuardService({ reserve: jest.fn() } as never);
    jest.spyOn(mockQuotaGuard, 'check').mockResolvedValue({ allowed: true, quotaType: 'test', limit: 30, used: 0, remaining: 30, resetAt: null, upgradeAvailable: false });
    jest.spyOn(mockQuotaGuard, 'buildError').mockReturnValue({ errorCode: 'QUOTA_EXCEEDED', message: '', quotaType: 'test', limit: 30, used: 30, remaining: 0, resetAt: null, upgradeAvailable: true });
    jest.spyOn(mockQuotaGuard, 'confirm').mockResolvedValue(undefined);
    jest.spyOn(mockQuotaGuard, 'cancel').mockResolvedValue(undefined);
    controller = new UserAiController(mockService, mockQuizRouter, mockLearningAnalysisRouter, mockQuotaGuard);
  });

  const req = (id = 'u1') => ({ user: { id } });

  // ═══════════════════════════════════════════════════════════════════
  // Profile
  // ═══════════════════════════════════════════════════════════════════

  describe('Profile', () => {
    it('GET profile returns profile or empty object', async () => {
      mockService.getProfile.mockResolvedValue({ learningGoal: 'exam' });
      const result = await controller.getProfile(req());
      expect(result).toEqual({ learningGoal: 'exam' });
      expect(mockService.getProfile).toHaveBeenCalledWith('u1');
    });

    it('GET profile returns {} when service returns null', async () => {
      mockService.getProfile.mockResolvedValue(null);
      const result = await controller.getProfile(req());
      expect(result).toEqual({});
    });

    it('PUT profile delegates to service', async () => {
      const dto: SaveLearningProfileDto = { learningGoal: 'exam' };
      mockService.saveProfile.mockResolvedValue({ id: 'p1' });
      const result = await controller.saveProfile(req(), dto);
      expect(mockService.saveProfile).toHaveBeenCalledWith('u1', dto);
      expect(result).toEqual({ id: 'p1' });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Settings
  // ═══════════════════════════════════════════════════════════════════

  describe('Settings', () => {
    it('GET settings', async () => {
      mockService.getSettings.mockResolvedValue({ allowAiAnalysis: true });
      const result = await controller.getSettings(req());
      expect(mockService.getSettings).toHaveBeenCalledWith('u1');
      expect(result).toEqual({ allowAiAnalysis: true });
    });

    it('PUT settings', async () => {
      const dto: UpdateAiSettingsDto = { allowAiAnalysis: false };
      mockService.updateSettings.mockResolvedValue({ id: 's1' });
      const result = await controller.updateSettings(req(), dto);
      expect(mockService.updateSettings).toHaveBeenCalledWith('u1', dto);
      expect(result).toEqual({ id: 's1' });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Model Credentials
  // ═══════════════════════════════════════════════════════════════════

  describe('Model Credentials', () => {
    it('GET model-credentials lists credentials', async () => {
      mockService.listCredentials.mockResolvedValue([{ id: 'c1' }]);
      const result = await controller.listCredentials(req());
      expect(mockService.listCredentials).toHaveBeenCalledWith('u1');
      expect(result).toEqual([{ id: 'c1' }]);
    });

    it('POST model-credentials creates credential', async () => {
      const dto: CreateCredentialDto = { provider: 'openai', apiKey: 'sk-xxx', keyAlias: 'MyKey' };
      mockService.createCredential.mockResolvedValue({ id: 'c1' });
      const result = await controller.createCredential(req(), dto);
      expect(mockService.createCredential).toHaveBeenCalledWith('u1', dto);
      expect(result).toEqual({ id: 'c1' });
    });

    it('PUT model-credentials/:id updates credential', async () => {
      const dto: UpdateCredentialDto = { apiKey: 'sk-updated', keyAlias: 'Updated' };
      mockService.updateCredential.mockResolvedValue({ id: 'c1' });
      const result = await controller.updateCredential(req(), 'c1', dto);
      expect(mockService.updateCredential).toHaveBeenCalledWith('u1', 'c1', dto);
      expect(result).toEqual({ id: 'c1' });
    });

    it('DELETE model-credentials/:id returns { ok: true }', async () => {
      mockService.deleteCredential.mockResolvedValue(undefined);
      const result = await controller.deleteCredential(req(), 'c1');
      expect(mockService.deleteCredential).toHaveBeenCalledWith('u1', 'c1');
      expect(result).toEqual({ ok: true });
    });

    it('POST model-credentials/:id/test tests credential', async () => {
      mockService.testCredential.mockResolvedValue({ success: true });
      const result = await controller.testCredential(req(), 'c1');
      expect(mockService.testCredential).toHaveBeenCalledWith('u1', 'c1');
      expect(result).toEqual({ success: true });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Analysis Jobs
  // ═══════════════════════════════════════════════════════════════════

  describe('Analysis Jobs', () => {
    it('quiz_generation routes through QuizExecutionRouter', async () => {
      const dto: CreateAnalysisJobDto = { jobType: 'quiz_generation', targetType: 'knowledge_base', targetId: 'kb1' };
      mockQuizRouter.generateQuiz.mockResolvedValue({ jobId: 'j1', status: 'queued' });
      const result = await controller.createAnalysisJob(req(), dto);
      expect(mockQuizRouter.generateQuiz).toHaveBeenCalledWith('u1', dto);
      expect(mockService.createAnalysisJob).not.toHaveBeenCalled();
      expect(result).toEqual({ jobId: 'j1', status: 'queued' });
    });

    it('non-quiz/non-analysis jobType calls legacy service', async () => {
      const dto: CreateAnalysisJobDto = { jobType: 'flashcard_generation', targetType: 'knowledge_base', targetId: 'kb1' };
      mockService.createAnalysisJob.mockResolvedValue({ jobId: 'j2' });
      const result = await controller.createAnalysisJob(req(), dto);
      expect(mockService.createAnalysisJob).toHaveBeenCalledWith('u1', dto);
      expect(mockQuizRouter.generateQuiz).not.toHaveBeenCalled();
      expect(result).toEqual({ jobId: 'j2' });
    });

    it('POST jobs/:jobId/cancel cancels job', async () => {
      mockService.cancelJob.mockResolvedValue({ status: 'cancelled' });
      const result = await controller.cancelJob(req(), 'j1');
      expect(mockService.cancelJob).toHaveBeenCalledWith('u1', 'j1');
      expect(result).toEqual({ status: 'cancelled' });
    });

    it('GET jobs/:jobId gets job detail', async () => {
      mockService.getJob.mockResolvedValue({ id: 'j1', status: 'running' });
      const result = await controller.getJob(req(), 'j1');
      expect(mockService.getJob).toHaveBeenCalledWith('u1', 'j1');
      expect(result).toEqual({ id: 'j1', status: 'running' });
    });

    it('GET jobs lists jobs with query filters', async () => {
      mockService.listJobs.mockResolvedValue([{ id: 'j1' }]);
      const result = await controller.listJobs(req(), 'running', '10');
      expect(mockService.listJobs).toHaveBeenCalledWith('u1', 'running', 10);
      expect(result).toEqual([{ id: 'j1' }]);
    });

    it('GET jobs without take returns undefined', async () => {
      mockService.listJobs.mockResolvedValue([]);
      await controller.listJobs(req(), undefined, undefined);
      expect(mockService.listJobs).toHaveBeenCalledWith('u1', undefined, undefined);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Quiz & Flashcard Publish
  // ═══════════════════════════════════════════════════════════════════

  describe('Publish', () => {
    it('POST quizzes/:quizId/publish', async () => {
      mockService.publishQuiz.mockResolvedValue({ status: 'published' });
      const result = await controller.publishQuiz(req(), 'q1');
      expect(mockService.publishQuiz).toHaveBeenCalledWith('u1', 'q1');
      expect(result).toEqual({ status: 'published' });
    });

    it('POST flashcards/:cardId/publish', async () => {
      mockService.publishFlashcard.mockResolvedValue({ status: 'published' });
      const result = await controller.publishFlashcard(req(), 'fc1');
      expect(mockService.publishFlashcard).toHaveBeenCalledWith('u1', 'fc1');
      expect(result).toEqual({ status: 'published' });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Analysis Results
  // ═══════════════════════════════════════════════════════════════════

  describe('Analysis Results', () => {
    it('GET analyses/:id', async () => {
      mockService.getAnalysis.mockResolvedValue({ id: 'a1' });
      const result = await controller.getAnalysis(req(), 'a1');
      expect(mockService.getAnalysis).toHaveBeenCalledWith('u1', 'a1');
      expect(result).toEqual({ id: 'a1' });
    });

    it('GET analyses with filters', async () => {
      mockService.listAnalyses.mockResolvedValue([{ id: 'a1' }]);
      const result = await controller.listAnalyses(req(), 'material', 'm1', '5');
      expect(mockService.listAnalyses).toHaveBeenCalledWith('u1', 'material', 'm1', 5);
      expect(result).toEqual([{ id: 'a1' }]);
    });

    it('GET analyses without filters', async () => {
      mockService.listAnalyses.mockResolvedValue([]);
      await controller.listAnalyses(req());
      expect(mockService.listAnalyses).toHaveBeenCalledWith('u1', undefined, undefined, undefined);
    });

    it('GET recommendations with filters', async () => {
      mockService.listRecommendations.mockResolvedValue([{ id: 'r1' }]);
      const result = await controller.listRecommendations(req(), 'material', 'm1', 'active', '5');
      expect(mockService.listRecommendations).toHaveBeenCalledWith('u1', 'material', 'm1', 'active', 5);
      expect(result).toEqual([{ id: 'r1' }]);
    });

    it('GET recommendations without filters', async () => {
      mockService.listRecommendations.mockResolvedValue([]);
      await controller.listRecommendations(req());
      expect(mockService.listRecommendations).toHaveBeenCalledWith('u1', undefined, undefined, undefined, undefined);
    });

    it('GET weak-points with filters', async () => {
      mockService.listWeakPoints.mockResolvedValue([{ id: 'w1' }]);
      const result = await controller.listWeakPoints(req(), 'material', 'm1', 'active', '5');
      expect(mockService.listWeakPoints).toHaveBeenCalledWith('u1', 'material', 'm1', 'active', 5);
      expect(result).toEqual([{ id: 'w1' }]);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Quizzes
  // ═══════════════════════════════════════════════════════════════════

  describe('Quizzes', () => {
    it('GET quizzes/:quizId', async () => {
      mockService.getQuiz.mockResolvedValue({ id: 'q1', title: 'Test' });
      const result = await controller.getQuiz(req(), 'q1');
      expect(mockService.getQuiz).toHaveBeenCalledWith('u1', 'q1');
      expect(result).toEqual({ id: 'q1', title: 'Test' });
    });

    it('GET quizzes/:quizId/questions', async () => {
      mockService.getQuizQuestions.mockResolvedValue([{ id: 'qq1' }]);
      const result = await controller.getQuizQuestions(req(), 'q1');
      // M-QUIZ-01-01: 第三个参数 attemptId 为 undefined（未提供时安全默认不返回答案）
      expect(mockService.getQuizQuestions).toHaveBeenCalledWith('u1', 'q1', undefined);
      expect(result).toEqual([{ id: 'qq1' }]);
    });

    it('GET quizzes with filters', async () => {
      mockService.listQuizzes.mockResolvedValue([{ id: 'q1' }]);
      const result = await controller.listQuizzes(req(), 'kb1', 'ready', '10');
      expect(mockService.listQuizzes).toHaveBeenCalledWith('u1', 'kb1', 'ready', 10);
      expect(result).toEqual([{ id: 'q1' }]);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Reanalysis
  // ═══════════════════════════════════════════════════════════════════

  describe('Reanalysis', () => {
    it('POST reanalyze triggers reanalysis', async () => {
      mockService.triggerReanalysis.mockResolvedValue({ jobId: 'j1' });
      const result = await controller.triggerReanalysis(req(), 'material', 'm1');
      expect(mockService.triggerReanalysis).toHaveBeenCalledWith('u1', 'material', 'm1');
      expect(result).toEqual({ jobId: 'j1' });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Notifications
  // ═══════════════════════════════════════════════════════════════════

  describe('Notifications', () => {
    it('GET notifications with take', async () => {
      mockService.listNotifications.mockResolvedValue([{ id: 'n1' }]);
      const result = await controller.listNotifications(req(), '20');
      expect(mockService.listNotifications).toHaveBeenCalledWith('u1', 20);
      expect(result).toEqual([{ id: 'n1' }]);
    });

    it('GET notifications without take', async () => {
      mockService.listNotifications.mockResolvedValue([]);
      await controller.listNotifications(req());
      expect(mockService.listNotifications).toHaveBeenCalledWith('u1', undefined);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Feedback
  // ═══════════════════════════════════════════════════════════════════

  describe('Feedback', () => {
    it('POST artifacts/:type/:id/feedback', async () => {
      const dto = { feedbackType: 'like', reason: 'good' };
      mockService.submitArtifactFeedback.mockResolvedValue({ ok: true });
      const result = await controller.submitArtifactFeedback(req(), 'quiz', 'q1', dto);
      expect(mockService.submitArtifactFeedback).toHaveBeenCalledWith('u1', 'quiz', 'q1', dto);
      expect(result).toEqual({ ok: true });
    });

    it('POST feedback', async () => {
      const dto = { category: 'bug', content: 'issue desc', email: 'a@b.com' };
      mockService.submitFeedback.mockResolvedValue({ ok: true });
      const result = await controller.submitFeedback(req(), dto);
      expect(mockService.submitFeedback).toHaveBeenCalledWith('u1', dto);
      expect(result).toEqual({ ok: true });
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Flashcards
  // ═══════════════════════════════════════════════════════════════════

  describe('Flashcards', () => {
    it('GET flashcards/:cardId', async () => {
      mockService.getFlashcard.mockResolvedValue({ id: 'fc1', front: 'Q', back: 'A' });
      const result = await controller.getFlashcard(req(), 'fc1');
      expect(mockService.getFlashcard).toHaveBeenCalledWith('u1', 'fc1');
      expect(result).toEqual({ id: 'fc1', front: 'Q', back: 'A' });
    });

    it('GET flashcards with filters', async () => {
      mockService.listFlashcards.mockResolvedValue([{ id: 'fc1' }]);
      const result = await controller.listFlashcards(req(), 'kp1', 'draft', '10');
      expect(mockService.listFlashcards).toHaveBeenCalledWith('u1', 'kp1', 'draft', 10);
      expect(result).toEqual([{ id: 'fc1' }]);
    });

    it('GET flashcards without filters', async () => {
      mockService.listFlashcards.mockResolvedValue([]);
      await controller.listFlashcards(req());
      expect(mockService.listFlashcards).toHaveBeenCalledWith('u1', undefined, undefined, undefined);
    });
  });
});
