import { describe, expect, it, vi } from 'vitest';

// Mock merge-charges helper before importing providers that use it
vi.mock('@modules/charges/helpers/merge-charges.helper.js', () => ({
  mergeChargesExecutor: vi.fn(),
}));

import { createMockTransaction, createMockDocument } from './test-helpers.js';

// Mock the module imports to avoid dependency issues
vi.mock('graphql-modules', () => ({
  Injectable: () => (target: any) => target,
  Inject: () => (target: any, propertyKey: string | symbol, parameterIndex: number) => {},
  Injector: class {},
  InjectionToken: class {
    constructor(public description?: string) {}
  },
  Scope: { Operation: 'Operation' },
  CONTEXT: Symbol('CONTEXT'),
}));

vi.mock('../../charges/providers/charges.provider.js', () => ({
  ChargesProvider: class {},
}));

vi.mock('../../documents/providers/documents.provider.js', () => ({
  DocumentsProvider: class {},
}));

vi.mock('../../transactions/providers/transactions.provider.js', () => ({
  TransactionsProvider: class {},
}));


vi.mock('../../financial-entities/providers/clients.provider.js', () => ({
  ClientsProvider: class {},
}));

vi.mock('../../admin-context/providers/admin-context.provider.js', () => ({
  AdminContextProvider: class {},
}));

vi.mock('../../../shared/helpers/index.js', () => ({
  dateToTimelessDateString: (date: Date) => date.toISOString().split('T')[0],
}));

const createMockProvider = (mockChargesProvider?: {
    getChargesByFilters?: (filters: any) => Promise<any[]>;
    getChargeByIdLoader?: { load: (id: string) => Promise<any>};
  },
  mockTransactionsProvider?: {
    transactionsByChargeIDLoader: { load: (id: string) => Promise<any[]>;
  }},
  mockDocumentsProvider?: {
    getDocumentsByChargeIdLoader: { load: (id: string) => Promise<any[]>; };
}
) => {
  const mockAdminContextProvider = {
    getVerifiedAdminContext: vi.fn(() => Promise.resolve({ ownerId: ADMIN_BUSINESS_ID })),
  } as any;

  const mockCharges = mockChargesProvider ?? null;
  const mockTransactions = mockTransactionsProvider ?? null;
  const mockDocuments = mockDocumentsProvider ?? null;

  const mockInjector = {
    get: vi.fn((token: {name: string}) => {
      if (token.name === 'ChargesProvider') return mockCharges;
      if (token.name === 'TransactionsProvider') return mockTransactions;
      if (token.name === 'DocumentsProvider') return mockDocuments;
      if (token.name === 'ClientsProvider')
        return {
          getClientByIdLoader: {
            load: (businessId: string) => {
              const isRegisteredClient = businessId.startsWith('client-');
              return Promise.resolve(isRegisteredClient ? { id: businessId } : null);
            },
          },
        };
      return null;
    }),
  } as Injector;

  const mockContext = {
    injector: mockInjector,
  } as any;

  return new ChargesMatcherProvider(
    mockAdminContextProvider,
    mockCharges as any,
    mockTransactions as any,
    mockDocuments as any,
    mockContext,
  );
};

// Import after mocking
const { ChargesMatcherProvider } = await import('../providers/charges-matcher.provider.js');

type Injector = {
  get: (token: any) => any;
};

// Test constants
const ADMIN_BUSINESS_ID = 'user-123';

// Mock charge object
function createCharge(id: string, ownerId: string) {
  return {
    id,
    owner_id: ownerId,
    transactions_min_event_date: new Date('2024-01-15'),
    transactions_max_event_date: new Date('2024-01-15'),
  };
}

describe('ChargesMatcherProvider - Integration Tests', () => {
  describe('findMatchesForCharge', () => {
    it('should find matches for transaction charge with mock data', async () => {
      // Setup mock data
      const sourceChargeId = 'tx-charge-1';
      const sourceTransactions = [
        createMockTransaction({
          charge_id: sourceChargeId,
          amount: "100",
          currency: 'USD',
          event_date: new Date('2024-01-15'),
        }),
      ];

      const candidateCharge1Id = 'doc-charge-1';
      const candidateDocuments1 = [
        createMockDocument({
          charge_id: candidateCharge1Id,
          total_amount: 100,
          currency_code: 'USD',
          date: new Date('2024-01-15'),
        }),
      ];

      const candidateCharge2Id = 'doc-charge-2';
      const candidateDocuments2 = [
        createMockDocument({
          charge_id: candidateCharge2Id,
          total_amount: 110,
          currency_code: 'USD',
          date: new Date('2024-01-16'),
        }),
      ];

      // Mock providers
      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
            if (id === candidateCharge1Id) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
            if (id === candidateCharge2Id) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
            return Promise.resolve(new Error('Charge not found'));
          }),
        },
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(candidateCharge1Id, ADMIN_BUSINESS_ID),
            createCharge(candidateCharge2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve(sourceTransactions);
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve([]);
            if (id === candidateCharge1Id) return Promise.resolve(candidateDocuments1);
            if (id === candidateCharge2Id) return Promise.resolve(candidateDocuments2);
            return Promise.resolve([]);
          }),
        },
      };

      // Execute
      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      // Verify
      expect(result.matches).toHaveLength(2);
      expect(result.matches[0].chargeId).toBe(candidateCharge1Id); // Perfect match
      expect(result.matches[0].confidenceScore).toBeGreaterThan(0.95);
      expect(result.matches[1].chargeId).toBe(candidateCharge2Id); // Partial match
      expect(result.matches[1].confidenceScore).toBeLessThan(0.95);
    });

    it('should find matches for document charge with mock data', async () => {
      // Setup mock data
      const sourceChargeId = 'doc-charge-1';
      const sourceDocuments = [
        createMockDocument({
          charge_id: sourceChargeId,
          total_amount: 200,
          currency_code: 'USD',
          date: new Date('2024-02-15'),
        }),
      ];

      const candidateCharge1Id = 'tx-charge-1';
      const candidateTransactions1 = [
        createMockTransaction({
          charge_id: candidateCharge1Id,
          amount: "200",
          currency: 'USD',
          event_date: new Date('2024-02-15'),
        }),
      ];

      const candidateCharge2Id = 'tx-charge-2';
      const candidateTransactions2 = [
        createMockTransaction({
          charge_id: candidateCharge2Id,
          amount: "195",
          currency: 'USD',
          event_date: new Date('2024-02-16'),
        }),
      ];

      // Mock providers
      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
            return Promise.resolve(createCharge(id, ADMIN_BUSINESS_ID));
          }),
        },
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(candidateCharge1Id, ADMIN_BUSINESS_ID),
            createCharge(candidateCharge2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === candidateCharge1Id) return Promise.resolve(candidateTransactions1);
            if (id === candidateCharge2Id) return Promise.resolve(candidateTransactions2);
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve(sourceDocuments);
            return Promise.resolve([]);
          }),
        },
      };

      // Execute
      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      // Verify
      expect(result.matches).toHaveLength(2);
      expect(result.matches[0].chargeId).toBe(candidateCharge1Id);
      expect(result.matches[0].confidenceScore).toBeGreaterThan(0.95);
    });

    it('should throw error if charge not found', async () => {
      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(new Error('Not found'))),
        },
      };

      const provider = createMockProvider(mockChargesProvider);

      await expect(
        provider.findMatchesForCharge('non-existent'),
      ).rejects.toThrow(/Source charge not found/);
    });

    it('should throw error if charge is already matched', async () => {
      const chargeId = 'matched-charge';

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(chargeId, ADMIN_BUSINESS_ID))),
        },
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn(() => Promise.resolve([createMockTransaction()])),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn(() => Promise.resolve([createMockDocument()])),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);

      await expect(provider.findMatchesForCharge(chargeId)).rejects.toThrow();
    });

    it('should throw error if charge has no transactions or documents', async () => {
      const chargeId = 'empty-charge';

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(chargeId, ADMIN_BUSINESS_ID))),
        },
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn(() => Promise.resolve([])),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn(() => Promise.resolve([])),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);

      await expect(provider.findMatchesForCharge(chargeId)).rejects.toThrow(
        /no transactions or documents/,
      );
    });

    it('should return empty matches if no candidates found', async () => {
      const sourceChargeId = 'tx-charge-only';

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
        },
        getChargesByFilters: vi.fn(() => Promise.resolve([])),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn(() => Promise.resolve([createMockTransaction()])),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn(() => Promise.resolve([])),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      expect(result.matches).toEqual([]);
    });

    it('should filter out matched candidates (having both transactions and documents)', async () => {
      const sourceChargeId = 'tx-charge-1';
      const matchedCandidateId = 'matched-charge';
      const validCandidateId = 'doc-charge-1';

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
        },
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(matchedCandidateId, ADMIN_BUSINESS_ID),
            createCharge(validCandidateId, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve([createMockTransaction()]);
            if (id === matchedCandidateId) return Promise.resolve([createMockTransaction()]); // Has transactions
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === matchedCandidateId) return Promise.resolve([createMockDocument()]); // Also has documents - MATCHED
            if (id === validCandidateId) return Promise.resolve([createMockDocument()]); // Only documents - VALID
            return Promise.resolve([]);
          }),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      // Should only include validCandidateId, not matchedCandidateId
      expect(result.matches).toHaveLength(1);
      expect(result.matches[0].chargeId).toBe(validCandidateId);
    });

    it('should handle date window filtering correctly', async () => {
      const sourceChargeId = 'tx-charge-1';
      const sourceDate = new Date('2024-06-15');

      const withinWindowId = 'doc-charge-within';
      const outsideWindowId = 'doc-charge-outside';

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
        },
        getChargesByFilters: vi.fn(params => {
          // Verify date window parameters
          expect(params.fromAnyDate).toBeDefined();
          expect(params.toAnyDate).toBeDefined();
          return Promise.resolve([
            createCharge(withinWindowId, ADMIN_BUSINESS_ID),
            createCharge(outsideWindowId, ADMIN_BUSINESS_ID),
          ]);
        }),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) {
              return Promise.resolve([createMockTransaction({ event_date: sourceDate })]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve([]);
            if (id === withinWindowId) {
              // Within 12-month window
              return Promise.resolve([
                createMockDocument({ date: new Date('2024-06-20') }),
              ]);
            }
            if (id === outsideWindowId) {
              // Outside 12-month window
              return Promise.resolve([
                createMockDocument({ date: new Date('2025-07-15') }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      // Should only include candidate within window
      expect(result.matches).toHaveLength(1);
      expect(result.matches[0].chargeId).toBe(withinWindowId);
    });

    it('should throw error if user ID not in context', async () => {
      const mockAdminContextProvider = {
        getVerifiedAdminContext: vi.fn(() => Promise.reject(new Error('Admin context not found for the authenticated user.'))),
      } as any;

      const provider = new ChargesMatcherProvider(
        mockAdminContextProvider,
        null as any,
        null as any,
        null as any,
        { injector: {} } as any,
      );

      await expect(provider.findMatchesForCharge('any-id')).rejects.toThrow(
        /Admin context not found/,
      );
    });

    it('should return top 5 matches when more candidates exist', async () => {
      const sourceChargeId = 'tx-charge-1';

      // Create 7 candidate charges
      const candidateIds = Array.from({ length: 7 }, (_, i) => `doc-charge-${i + 1}`);

      const mockChargesProvider = {
        getChargeByIdLoader: {
          load: vi.fn(() => Promise.resolve(createCharge(sourceChargeId, ADMIN_BUSINESS_ID))),
        },
        getChargesByFilters: vi.fn(() =>
          Promise.resolve(candidateIds.map(id => createCharge(id, ADMIN_BUSINESS_ID))),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) {
              return Promise.resolve([createMockTransaction({ amount: "100" })]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === sourceChargeId) return Promise.resolve([]);
            // Create documents with varying amounts for different confidence scores
            const index = parseInt(id.split('-')[2]);
            return Promise.resolve([createMockDocument({ total_amount: 100 + index })]);
          }),
        },
      };

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      const result = await provider.findMatchesForCharge(sourceChargeId);

      // Should return maximum of 5 matches
      expect(result.matches).toHaveLength(5);
      // Verify sorted by confidence (descending)
      for (let i = 0; i < result.matches.length - 1; i++) {
        expect(result.matches[i].confidenceScore).toBeGreaterThanOrEqual(
          result.matches[i + 1].confidenceScore,
        );
      }
    });
  });
});
