import {
  HttpNeighborhoodScoreService,
  NeighborhoodScoreServiceImpl,
  PrismaNeighborhoodScoreService,
} from '../services/neighborhood-score.service';

describe('NeighborhoodScoreServiceImpl', () => {
  let service: NeighborhoodScoreServiceImpl;
  let mockPrisma: {
    neighborhoodScore: { findUnique: ReturnType<typeof vi.fn>; upsert: ReturnType<typeof vi.fn> };
    pOI: { count: ReturnType<typeof vi.fn> };
  };
  let mockLogger: { log: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPrisma = {
      neighborhoodScore: {
        findUnique: vi.fn(),
        upsert: vi.fn(),
      },
      pOI: { count: vi.fn() },
    };
    mockLogger = { log: vi.fn() };

    service = new NeighborhoodScoreServiceImpl(mockPrisma as any, mockLogger as any);
  });

  describe('getScore', () => {
    it('returns existing score from database', async () => {
      const stored = {
        district: 'Quận 1',
        city: 'Hồ Chí Minh',
        educationScore: 8,
        healthcareScore: 7,
        transportScore: 9,
        shoppingScore: 6,
        greeneryScore: 5,
        safetyScore: 4,
        totalScore: 68.5,
        poiCounts: { education: 12, healthcare: 5 },
        calculatedAt: new Date(),
      };
      mockPrisma.neighborhoodScore.findUnique.mockResolvedValue(stored);

      const result = await service.getScore('Quận 1', 'Hồ Chí Minh');

      expect(result).not.toBeNull();
      expect(result!.district).toBe('Quận 1');
      expect(result!.totalScore).toBe(68.5);
      expect(result!.poiCounts).toEqual({ education: 12, healthcare: 5 });
    });

    it('returns null when no score exists', async () => {
      mockPrisma.neighborhoodScore.findUnique.mockResolvedValue(null);

      const result = await service.getScore('Quận 99', 'Hồ Chí Minh');

      expect(result).toBeNull();
    });
  });

  describe('calculateAndSave', () => {
    it('calculates scores from POI counts and upserts', async () => {
      // Simulate POI counts: education=15 (max), healthcare=4 (50%), transport=6 (50%),
      // shopping=5 (50%), greenery=3 (50%), safety=2 (50%)
      const poiCountsByCategory = [15, 4, 6, 5, 3, 2];
      let callIndex = 0;
      mockPrisma.pOI.count.mockImplementation(() => {
        return Promise.resolve(poiCountsByCategory[callIndex++]!);
      });

      mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
        return Promise.resolve(create);
      });

      const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');

      // education: 15/15 * 10 = 10 → 10 * 20/10 = 20
      // healthcare: 4/8 * 10 = 5 → 5 * 20/10 = 10
      // transport: 6/12 * 10 = 5 → 5 * 20/10 = 10
      // shopping: 5/10 * 10 = 5 → 5 * 15/10 = 7.5
      // greenery: 3/6 * 10 = 5 → 5 * 15/10 = 7.5
      // safety: 2/4 * 10 = 5 → 5 * 10/10 = 5
      // total = 20 + 10 + 10 + 7.5 + 7.5 + 5 = 60
      expect(result.educationScore).toBe(10);
      expect(result.healthcareScore).toBe(5);
      expect(result.totalScore).toBe(60);
      expect(mockPrisma.neighborhoodScore.upsert).toHaveBeenCalledTimes(1);
    });

    it('caps category scores at 10', async () => {
      // All categories have way more POIs than max
      mockPrisma.pOI.count.mockResolvedValue(100);
      mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
        return Promise.resolve(create);
      });

      const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');

      // All scores capped at 10 → total = sum of weights = 100
      expect(result.educationScore).toBe(10);
      expect(result.healthcareScore).toBe(10);
      expect(result.transportScore).toBe(10);
      expect(result.shoppingScore).toBe(10);
      expect(result.greeneryScore).toBe(10);
      expect(result.safetyScore).toBe(10);
      expect(result.totalScore).toBe(100);
    });

    it('returns 0 scores when no POIs exist', async () => {
      mockPrisma.pOI.count.mockResolvedValue(0);
      mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
        return Promise.resolve(create);
      });

      const result = await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');

      expect(result.educationScore).toBe(0);
      expect(result.totalScore).toBe(0);
    });

    it('logs the calculated score', async () => {
      mockPrisma.pOI.count.mockResolvedValue(5);
      mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => {
        return Promise.resolve(create);
      });

      await service.calculateAndSave('Quận 1', 'Hồ Chí Minh');

      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Quận 1'),
        'NeighborhoodScoreService',
      );
    });
  });
});

describe('HttpNeighborhoodScoreService', () => {
  let httpService: HttpNeighborhoodScoreService;
  let prismaFallback: PrismaNeighborhoodScoreService;
  let mockPrisma: {
    neighborhoodScore: { findUnique: ReturnType<typeof vi.fn>; upsert: ReturnType<typeof vi.fn> };
    pOI: { count: ReturnType<typeof vi.fn> };
  };
  let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn> };
  let mockAiClient: { scoreNeighborhood: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockPrisma = {
      neighborhoodScore: { findUnique: vi.fn(), upsert: vi.fn() },
      pOI: { count: vi.fn() },
    };
    mockLogger = { log: vi.fn(), warn: vi.fn() };
    mockAiClient = { scoreNeighborhood: vi.fn() };
    prismaFallback = new PrismaNeighborhoodScoreService(
      mockPrisma as any,
      mockLogger as any,
    );
    httpService = new HttpNeighborhoodScoreService(
      mockPrisma as any,
      mockLogger as any,
      mockAiClient as any,
      prismaFallback,
    );
  });

  it('persists AI service response when scoreNeighborhood succeeds', async () => {
    mockPrisma.pOI.count.mockResolvedValue(6);
    mockAiClient.scoreNeighborhood.mockResolvedValue({
      district: 'Quận 1',
      city: 'Hồ Chí Minh',
      education_score: 8.5,
      healthcare_score: 7,
      transport_score: 9,
      shopping_score: 6,
      greenery_score: 5.5,
      safety_score: 4,
      total_score: 71.2,
      poi_counts: { education: 6, healthcare: 6, transport: 6, shopping: 6, greenery: 6, safety: 6 },
      algorithm_version: 'neighborhood-heuristic-v1',
    });
    mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => Promise.resolve(create));

    const result = await httpService.calculateAndSave('Quận 1', 'Hồ Chí Minh');

    expect(mockAiClient.scoreNeighborhood).toHaveBeenCalledOnce();
    expect(result.totalScore).toBe(71.2);
    expect(result.educationScore).toBe(8.5);
    expect(mockPrisma.neighborhoodScore.upsert).toHaveBeenCalledOnce();
  });

  it('falls back to prisma scoring when AI service throws', async () => {
    mockPrisma.pOI.count.mockResolvedValue(0);
    mockAiClient.scoreNeighborhood.mockRejectedValue(new Error('AI service down'));
    mockPrisma.neighborhoodScore.upsert.mockImplementation(({ create }) => Promise.resolve(create));

    const result = await httpService.calculateAndSave('Quận 7', 'Hồ Chí Minh');

    expect(mockLogger.warn).toHaveBeenCalledWith(
      expect.stringContaining('falling back to prisma scoring'),
      'NeighborhoodScoreService',
    );
    expect(result.totalScore).toBe(0);
    expect(mockPrisma.neighborhoodScore.upsert).toHaveBeenCalledOnce();
  });

  it('delegates getScore to prisma fallback', async () => {
    mockPrisma.neighborhoodScore.findUnique.mockResolvedValue(null);

    const result = await httpService.getScore('Quận 99', 'Hồ Chí Minh');

    expect(result).toBeNull();
    expect(mockPrisma.neighborhoodScore.findUnique).toHaveBeenCalledOnce();
    expect(mockAiClient.scoreNeighborhood).not.toHaveBeenCalled();
  });
});
