// ABOUTME: Unit tests for horse matching logic in RaceService
// ABOUTME: Tests all 6 matching priorities, name normalization, and duplicate detection

import { RaceService } from '../race-service';
import { TabApiClient } from '../../api/tab';

// Mock dependencies
jest.mock('../../utils/logger', () => ({
  __esModule: true,
  default: {
    info: jest.fn(),
    debug: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

jest.mock('prom-client', () => ({
  Counter: jest.fn().mockImplementation(() => ({
    inc: jest.fn(),
    labels: jest.fn().mockReturnThis(),
  })),
  Histogram: jest.fn().mockImplementation(() => ({
    observe: jest.fn(),
    labels: jest.fn().mockReturnThis(),
  })),
}));

jest.mock('@opentelemetry/api', () => ({
  trace: {
    getTracer: jest.fn(() => ({
      startSpan: jest.fn(() => ({
        setAttributes: jest.fn(),
        setAttribute: jest.fn(),
        setStatus: jest.fn(),
        end: jest.fn(),
      })),
    })),
  },
  SpanStatusCode: {
    OK: 1,
    ERROR: 2,
  },
}));

describe('Horse Matching Logic', () => {
  let raceService: RaceService;
  let mockApiClient: jest.Mocked<TabApiClient>;
  let mockPrisma: any;
  let mockTx: any;

  beforeEach(() => {
    jest.clearAllMocks();

    // Setup mock Prisma transaction
    mockTx = {
      horse: {
        findUnique: jest.fn(),
        findFirst: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
      },
      runner: {
        findFirst: jest.fn(),
        upsert: jest.fn(),
      },
    };

    mockPrisma = {
      $transaction: jest.fn((callback) => callback(mockTx)),
      horse: mockTx.horse,
      runner: mockTx.runner,
    };

    mockApiClient = {
      getRaceById: jest.fn(),
    } as any;

    raceService = new RaceService(mockApiClient, mockPrisma);
  });

  describe('findOrCreateHorse - Priority 1: Match by tabEntrantId', () => {
    it('should match existing horse by tabEntrantId', async () => {
      const existingHorse = {
        id: 'horse-uuid-1',
        tabEntrantId: 'entrant-uuid-1',
        name: 'Test Horse',
      };

      mockTx.horse.findUnique.mockResolvedValueOnce(existingHorse);

      const runnerData = {
        entrant_id: 'entrant-uuid-1',
        name: 'Test Horse',
        horse_id: 12345,
      };

      // Call private method via processRunners
      // Note: We'll need to expose findOrCreateHorse for testing or test it through processRunners
      // For now, testing via integration with processRunners
      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-1');
      expect(mockTx.horse.findUnique).toHaveBeenCalledWith({
        where: { tabEntrantId: 'entrant-uuid-1' },
      });
    });

    it('should not match by tabEntrantId if null', async () => {
      const runnerData = {
        entrant_id: null,
        name: 'Test Horse',
        horse_id: 12345,
      };

      // When entrant_id is null, first call to findUnique is for horse_id
      mockTx.horse.findUnique.mockResolvedValueOnce({
        id: 'horse-uuid-2',
        tabHorseId: 12345,
      });

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-2');
      expect(mockTx.horse.findUnique).toHaveBeenCalledWith({
        where: { tabHorseId: 12345 },
      });
    });
  });

  describe('findOrCreateHorse - Priority 2: Match by tabHorseId', () => {
    it('should match existing horse by tabHorseId when entrant_id not found', async () => {
      const existingHorse = {
        id: 'horse-uuid-2',
        tabHorseId: 12345,
        name: 'Test Horse',
      };

      mockTx.horse.findUnique
        .mockResolvedValueOnce(null)          // entrant_id lookup fails
        .mockResolvedValueOnce(existingHorse); // horse_id lookup succeeds

      const runnerData = {
        entrant_id: 'unknown-entrant',
        name: 'Test Horse',
        horse_id: 12345,
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-2');
      expect(mockTx.horse.findUnique).toHaveBeenCalledWith({
        where: { tabHorseId: 12345 },
      });
    });
  });

  describe('findOrCreateHorse - Priority 3: Match by harnessNzHorseId', () => {
    it('should match existing horse by harnessNzHorseId', async () => {
      const existingHorse = {
        id: 'horse-uuid-3',
        harnessNzHorseId: BigInt(98765),
        name: 'Test Horse',
      };

      // Only harness_nz_horse_id is provided, so only one findUnique call
      mockTx.horse.findUnique.mockResolvedValueOnce(existingHorse);

      const runnerData = {
        name: 'Test Horse',
        harness_nz_horse_id: 98765,
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-3');
      expect(mockTx.horse.findUnique).toHaveBeenCalledWith({
        where: { harnessNzHorseId: BigInt(98765) },
      });
    });

    it('should handle harnessNzHorseId as string', async () => {
      const existingHorse = {
        id: 'horse-uuid-3',
        harnessNzHorseId: BigInt(98765),
        name: 'Test Horse',
      };

      mockTx.horse.findUnique.mockResolvedValueOnce(existingHorse);

      const runnerData = {
        name: 'Test Horse',
        harness_nz_horse_id: '98765',  // String instead of number
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-3');
      expect(mockTx.horse.findUnique).toHaveBeenCalledWith({
        where: { harnessNzHorseId: BigInt(98765) },
      });
    });
  });

  describe('findOrCreateHorse - Priority 4: Match by breeding', () => {
    it('should match existing horse by name + sire + dam', async () => {
      const existingHorse = {
        id: 'horse-uuid-4',
        normalizedName: 'test horse',
        sire: 'Big Sire',
        dam: 'Fast Dam',
      };

      mockTx.horse.findUnique
        .mockResolvedValue(null);  // All external ID lookups fail

      mockTx.horse.findFirst.mockResolvedValueOnce(existingHorse);

      const runnerData = {
        name: 'Test Horse',
        sire: 'Big Sire',
        dam: 'Fast Dam',
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-4');
      expect(mockTx.horse.findFirst).toHaveBeenCalledWith({
        where: {
          normalizedName: 'test horse',
          sire: 'Big Sire',
          dam: 'Fast Dam',
        },
      });
    });

    it('should not match by breeding if sire or dam missing', async () => {
      mockTx.horse.findUnique.mockResolvedValue(null);
      mockTx.horse.findFirst.mockResolvedValue(null);
      mockTx.horse.create.mockResolvedValue({ id: 'new-horse-id' });

      const runnerData = {
        name: 'Test Horse',
        sire: 'Big Sire',
        // dam is missing
      };

      await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      // Should skip to name-only matching, not breeding
      expect(mockTx.horse.findFirst).toHaveBeenCalled();
      const firstCall = mockTx.horse.findFirst.mock.calls[0][0];
      expect(firstCall.where).not.toHaveProperty('sire');
    });
  });

  describe('findOrCreateHorse - Priority 5: Fuzzy match by normalized name', () => {
    it('should match horse by normalized name only (with warning)', async () => {
      const existingHorse = {
        id: 'horse-uuid-5',
        normalizedName: 'test horse',
        name: 'Test Horse',
      };

      // No external IDs provided, breeding check skipped (no sire/dam), goes directly to name-only match
      mockTx.horse.findFirst.mockResolvedValueOnce(existingHorse); // Only one call needed

      const runnerData = {
        name: 'Test Horse',
        // No entrant_id, horse_id, harness_nz_horse_id, sire, or dam
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('horse-uuid-5');
      expect(mockTx.horse.findFirst).toHaveBeenCalledWith({
        where: { normalizedName: 'test horse' },
      });
    });
  });

  describe('findOrCreateHorse - Priority 6: Create new horse', () => {
    it('should create new horse when no matches found', async () => {
      const newHorse = {
        id: 'new-horse-uuid',
        tabEntrantId: 'new-entrant-id',
        name: 'New Horse',
        normalizedName: 'new horse',
      };

      mockTx.horse.findUnique.mockResolvedValue(null);
      mockTx.horse.findFirst.mockResolvedValue(null);
      mockTx.horse.create.mockResolvedValue(newHorse);

      const runnerData = {
        entrant_id: 'new-entrant-id',
        name: 'New Horse',
        horse_id: 12345,
        harness_nz_horse_id: 98765,
        sire: 'Big Sire',
        dam: 'Fast Dam',
        dam_sire: 'Old Sire',
        sex: 'Gelding',
        colour: 'Bay',
        country: 'AUS',
      };

      const result = await (raceService as any).findOrCreateHorse(mockTx, runnerData);

      expect(result).toBe('new-horse-uuid');
      expect(mockTx.horse.create).toHaveBeenCalledWith({
        data: {
          tabEntrantId: 'new-entrant-id',
          tabHorseId: 12345,
          harnessNzHorseId: BigInt(98765),
          name: 'New Horse',
          normalizedName: 'new horse',
          sire: 'Big Sire',
          dam: 'Fast Dam',
          damSire: 'Old Sire',
          sex: 'Gelding',
          colour: 'Bay',
          country: 'AUS',
          raceCount: 1,
        },
      });
    });
  });

  describe('normalizeHorseName', () => {
    it('should convert to lowercase', () => {
      const result = (raceService as any).normalizeHorseName('TEST HORSE');
      expect(result).toBe('test horse');
    });

    it('should remove apostrophes', () => {
      const result = (raceService as any).normalizeHorseName("Artie's Storm");
      expect(result).toBe('arties storm');
    });

    it('should handle different apostrophe types', () => {
      const result1 = (raceService as any).normalizeHorseName("Test'Horse");
      const result2 = (raceService as any).normalizeHorseName("Test'Horse");
      expect(result1).toBe('testhorse');
      expect(result2).toBe('testhorse');
    });

    it('should normalize whitespace', () => {
      const result = (raceService as any).normalizeHorseName('Test   Multiple  Spaces');
      expect(result).toBe('test multiple spaces');
    });

    it('should trim leading and trailing spaces', () => {
      const result = (raceService as any).normalizeHorseName('  Test Horse  ');
      expect(result).toBe('test horse');
    });

    it('should handle empty string', () => {
      const result = (raceService as any).normalizeHorseName('');
      expect(result).toBe('');
    });

    it('should preserve original name in horse record', () => {
      // This tests that the original name is passed through unchanged
      const originalName = "Artie's Storm";
      mockTx.horse.create.mockResolvedValue({ id: 'test-id' });

      const runnerData = {
        name: originalName,
      };

      (raceService as any).createHorse(mockTx, runnerData);

      expect(mockTx.horse.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            name: originalName,  // Original preserved
            normalizedName: 'arties storm',  // Normalized for matching
          }),
        })
      );
    });
  });

  describe('Duplicate detection', () => {
    it('should not create duplicate horses with same tabEntrantId', async () => {
      const existingHorse = {
        id: 'existing-horse-id',
        tabEntrantId: 'same-entrant-id',
      };

      mockTx.horse.findUnique.mockResolvedValueOnce(existingHorse);

      const runnerData1 = {
        entrant_id: 'same-entrant-id',
        name: 'Horse A',
      };

      const runnerData2 = {
        entrant_id: 'same-entrant-id',
        name: 'Horse B',  // Different name, same ID
      };

      const result1 = await (raceService as any).findOrCreateHorse(mockTx, runnerData1);
      mockTx.horse.findUnique.mockResolvedValueOnce(existingHorse);
      const result2 = await (raceService as any).findOrCreateHorse(mockTx, runnerData2);

      expect(result1).toBe('existing-horse-id');
      expect(result2).toBe('existing-horse-id');
      expect(mockTx.horse.create).not.toHaveBeenCalled();
    });
  });
});
