import { RoutingService } from '../services/routing.service';
import { type RoutingPoliciesRepository } from '../repositories/routing-policies.repository';
import { type RoutingDecisionsRepository } from '../repositories/routing-decisions.repository';
import { type RoutingManager } from '../managers/routing.manager';
import { type ReplayManager } from '../managers/replay.manager';
import { type RabbitMQService } from '@claw/shared-rabbitmq';
import { EntityNotFoundException } from '../../../common/errors';
import { RoutingMode } from '../../../generated/prisma';

const mockPolicy = {
  id: 'policy-1',
  name: 'Default Auto',
  routingMode: RoutingMode.AUTO,
  priority: 0,
  isActive: true,
  config: {},
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockDecision = {
  id: 'decision-1',
  messageId: 'msg-1',
  threadId: 'thread-1',
  selectedProvider: 'anthropic',
  selectedModel: 'claude-sonnet-4',
  routingMode: RoutingMode.AUTO,
  confidence: 0.75,
  reasonTags: ['auto'],
  privacyClass: 'cloud',
  costClass: 'medium',
  fallbackProvider: null,
  fallbackModel: null,
  createdAt: new Date(),
};

const mockPoliciesRepo = (): {
  create: jest.Mock;
  findById: jest.Mock;
  findAll: jest.Mock;
  countAll: jest.Mock;
  findActivePolicies: jest.Mock;
  update: jest.Mock;
  delete: jest.Mock;
} => ({
  create: jest.fn().mockResolvedValue(mockPolicy),
  findById: jest.fn().mockResolvedValue(mockPolicy),
  findAll: jest.fn().mockResolvedValue([mockPolicy]),
  countAll: jest.fn().mockResolvedValue(1),
  findActivePolicies: jest.fn().mockResolvedValue([mockPolicy]),
  update: jest.fn().mockResolvedValue({ ...mockPolicy, name: 'Updated' }),
  delete: jest.fn().mockResolvedValue(mockPolicy),
});

const mockDecisionsRepo = (): Record<string, jest.Mock> => ({
  create: jest.fn().mockResolvedValue(mockDecision),
  findById: jest.fn().mockResolvedValue(mockDecision),
  findByThreadId: jest.fn().mockResolvedValue([mockDecision]),
  countByThreadId: jest.fn().mockResolvedValue(1),
});

const mockRoutingManager = (): Partial<Record<keyof RoutingManager, jest.Mock>> => ({
  evaluateRoute: jest.fn().mockResolvedValue({
    selectedProvider: 'anthropic',
    selectedModel: 'claude-sonnet-4',
    routingMode: RoutingMode.AUTO,
    confidence: 0.75,
    reasonTags: ['auto'],
    privacyClass: 'cloud',
    costClass: 'medium',
    fallbackChain: [],
  }),
  buildFallbackChain: jest.fn().mockReturnValue([]),
});

const mockReplayManager = (): Partial<Record<keyof ReplayManager, jest.Mock>> => ({
  replayDecisions: jest.fn().mockResolvedValue({
    totalReplayed: 1,
    changed: 1,
    unchanged: 0,
    averageConfidenceOld: 0.75,
    averageConfidenceNew: 0.85,
    results: [
      {
        originalDecision: {
          selectedProvider: 'anthropic',
          selectedModel: 'claude-sonnet-4',
          confidence: 0.75,
          reasonTags: ['auto'],
          costClass: 'medium',
        },
        replayDecision: {
          selectedProvider: 'openai',
          selectedModel: 'gpt-4o',
          confidence: 0.85,
          reasonTags: ['auto', 'category_match'],
          costClass: 'medium',
          detectedCategory: 'coding',
          estimatedCostPer1M: 5,
          latencySlaMs: 3000,
        },
        changed: true,
        improvementScore: 1,
      },
    ],
  }),
});

const mockRabbitMQ = (): Partial<Record<keyof RabbitMQService, jest.Mock>> => ({
  publish: jest.fn().mockResolvedValue(void 0),
  subscribe: jest.fn().mockResolvedValue(void 0),
});

const mockRouterEducationManager = (): Record<string, jest.Mock> => ({
  calibrateDecision: jest
    .fn()
    .mockImplementation(async (decision) => ({ decision, changed: false })),
  ingestExecutionOutcome: jest.fn().mockResolvedValue(void 0),
  ingestFeedbackSignal: jest.fn().mockResolvedValue(void 0),
  getLatestSnapshot: jest.fn().mockResolvedValue(null),
  listModelProfiles: jest.fn().mockResolvedValue([]),
  listTopicProfiles: jest.fn().mockResolvedValue([]),
});

describe('RoutingService', () => {
  let service: RoutingService;
  let policiesRepo: ReturnType<typeof mockPoliciesRepo>;
  let decisionsRepo: ReturnType<typeof mockDecisionsRepo>;
  let routingManager: ReturnType<typeof mockRoutingManager>;
  let replayMgr: ReturnType<typeof mockReplayManager>;
  let rabbitMQ: ReturnType<typeof mockRabbitMQ>;
  let routerEducationManager: ReturnType<typeof mockRouterEducationManager>;

  beforeEach(() => {
    policiesRepo = mockPoliciesRepo();
    decisionsRepo = mockDecisionsRepo();
    routingManager = mockRoutingManager();
    replayMgr = mockReplayManager();
    rabbitMQ = mockRabbitMQ();
    routerEducationManager = mockRouterEducationManager();
    const promptBuilder = {
      invalidateCache: jest.fn(),
      fetchInstalledModels: jest.fn().mockResolvedValue([]),
      getInstalledModels: jest.fn().mockResolvedValue([]),
    };
    service = new RoutingService(
      policiesRepo as unknown as RoutingPoliciesRepository,
      decisionsRepo as unknown as RoutingDecisionsRepository,
      routingManager as unknown as RoutingManager,
      replayMgr as unknown as ReplayManager,
      {} as any,
      routerEducationManager as any,
      rabbitMQ as unknown as RabbitMQService,
      promptBuilder as any,
      { isFrontierAvailable: () => false } as any,
    );
  });

  describe('createPolicy', () => {
    it('should create a routing policy', async () => {
      const result = await service.createPolicy({
        name: 'Default Auto',
        routingMode: RoutingMode.AUTO,
        priority: 0,
        config: {},
      });

      expect(result.id).toBe('policy-1');
      expect(policiesRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'Default Auto' }),
      );
    });
  });

  describe('getPolicies', () => {
    it('should return paginated policies', async () => {
      const result = await service.getPolicies({ page: 1, limit: 20 });

      expect(result.data).toHaveLength(1);
      expect(result.meta.total).toBe(1);
    });

    it('should pass routing mode filter', async () => {
      await service.getPolicies({
        page: 1,
        limit: 20,
        routingMode: RoutingMode.AUTO,
      });

      expect(policiesRepo.findAll).toHaveBeenCalledWith(
        expect.objectContaining({ routingMode: RoutingMode.AUTO }),
        1,
        20,
      );
    });
  });

  describe('getPolicy', () => {
    it('should return policy by id', async () => {
      const result = await service.getPolicy('policy-1');

      expect(result.id).toBe('policy-1');
    });

    it('should throw EntityNotFoundException when not found', async () => {
      policiesRepo.findById.mockResolvedValue(null);

      await expect(service.getPolicy('nonexistent')).rejects.toThrow(EntityNotFoundException);
    });
  });

  describe('updatePolicy', () => {
    it('should update policy', async () => {
      const result = await service.updatePolicy('policy-1', { name: 'Updated' });

      expect(result.name).toBe('Updated');
      expect(policiesRepo.update).toHaveBeenCalledWith('policy-1', { name: 'Updated' });
    });

    it('should throw EntityNotFoundException when not found', async () => {
      policiesRepo.findById.mockResolvedValue(null);

      await expect(service.updatePolicy('nonexistent', { name: 'Updated' })).rejects.toThrow(
        EntityNotFoundException,
      );
    });
  });

  describe('deletePolicy', () => {
    it('should delete policy', async () => {
      const result = await service.deletePolicy('policy-1');

      expect(result.id).toBe('policy-1');
      expect(policiesRepo.delete).toHaveBeenCalledWith('policy-1');
    });
  });

  describe('evaluateRoute', () => {
    it('should evaluate route and return decision', async () => {
      const result = await service.evaluateRoute({
        messageContent: 'Hello world',
      });

      expect(result.selectedProvider).toBe('anthropic');
      expect(result.routingMode).toBe(RoutingMode.AUTO);
      expect(routingManager.evaluateRoute).toHaveBeenCalled();
      expect(routerEducationManager.calibrateDecision).toHaveBeenCalled();
    });

    it('should pass routing mode to manager', async () => {
      await service.evaluateRoute({
        messageContent: 'Hello',
        routingMode: RoutingMode.LOCAL_ONLY,
      });

      expect(routingManager.evaluateRoute).toHaveBeenCalledWith(
        expect.objectContaining({ userMode: RoutingMode.LOCAL_ONLY }),
      );
    });
  });

  describe('getDecisions', () => {
    it('should return paginated decisions for thread', async () => {
      const result = await service.getDecisions('thread-1', 1, 20);

      expect(result.data).toHaveLength(1);
      expect(result.meta.total).toBe(1);
      expect(decisionsRepo.findByThreadId).toHaveBeenCalledWith('thread-1', 1, 20);
    });
  });

  describe('replayRouting', () => {
    it('should delegate to replay manager and return batch result', async () => {
      const dto = { limit: 10, saveRun: false as const };
      const result = await service.replayRouting(dto);

      expect(result.totalReplayed).toBe(1);
      expect(result.changed).toBe(1);
      expect(result.unchanged).toBe(0);
      expect(result.averageConfidenceOld).toBe(0.75);
      expect(result.averageConfidenceNew).toBe(0.85);
      expect(result.results).toHaveLength(1);
      expect(replayMgr.replayDecisions).toHaveBeenCalledWith(dto);
    });

    it('should pass filters to replay manager', async () => {
      const dto = {
        threadId: 'thread-1',
        routingMode: 'AUTO',
        startDate: '2026-01-01',
        endDate: '2026-12-31',
        limit: 25,
        saveRun: false as const,
      };
      await service.replayRouting(dto);

      expect(replayMgr.replayDecisions).toHaveBeenCalledWith(dto);
    });
  });

  describe('event subscriptions', () => {
    it('should subscribe to events on module init', async () => {
      await service.onModuleInit();

      expect(rabbitMQ.subscribe).toHaveBeenCalledTimes(10);
    });
  });
});
