import { describe, expect, it, vi, beforeEach } from 'vitest';
import { createMockTransaction, createMockDocument } from './test-helpers.js';

// Mock the module imports
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('../../charges/helpers/merge-charges.helper.js', () => ({
  mergeChargesExecutor: vi.fn(),
}));

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

const createMockProvider = (mockChargesProvider?: {
    getChargesByFilters: (filters: any) => 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 ?? {
    getChargesByFilters: vi.fn(() => Promise.resolve([])),
  };

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

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

  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');
const { mergeChargesExecutor } = await import(
  '../../charges/helpers/merge-charges.helper.js'
);

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

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

// Helper to create charge
function createCharge(id: string, ownerId: string) {
  return {
    id,
    owner_id: ownerId,
  };
}

describe('ChargesMatcherProvider - Auto-Match Integration', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('autoMatchCharges', () => {
    it('should return 0 matches when database is empty', async () => {
      const provider = createMockProvider();
      const result = await provider.autoMatchCharges();

      expect(result.totalMatches).toBe(0);
      expect(result.mergedCharges).toEqual([]);
      expect(result.skippedCharges).toEqual([]);
      expect(result.errors).toEqual([]);
    });

    it('should return 0 matches when all charges are already matched', async () => {
      const matchedChargeId = 'matched-charge-1';

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

      const provider = createMockProvider(mockChargesProvider);
      const result = await provider.autoMatchCharges();

      expect(result.totalMatches).toBe(0);
      expect(result.mergedCharges).toEqual([]);
      expect(result.skippedCharges).toEqual([]);
    });

    it('should execute merge for single unmatched charge with good match', async () => {
      const txChargeId = 'tx-charge-1';
      const docChargeId = 'doc-charge-1';

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

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === txChargeId) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: txChargeId,
                  amount: "100",
                  currency: 'USD',
                  event_date: new Date('2024-01-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === docChargeId) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: docChargeId,
                  total_amount: 100,
                  currency_code: 'USD',
                  date: new Date('2024-01-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

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

      expect(result.totalMatches).toBe(1);
      expect(result.mergedCharges).toHaveLength(1);
      expect(result.mergedCharges[0].confidenceScore).toBeGreaterThanOrEqual(0.95);
      expect(result.skippedCharges).toEqual([]);
      expect(result.errors).toEqual([]);

      // Verify merge was called
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
    });

    it('should skip charges with ambiguous matches', async () => {
      const txChargeId = 'tx-charge-1';
      const docCharge1Id = 'doc-charge-1';
      const docCharge2Id = 'doc-charge-2';

      const mockChargesProvider = {
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(txChargeId, ADMIN_BUSINESS_ID),
            createCharge(docCharge1Id, ADMIN_BUSINESS_ID),
            createCharge(docCharge2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === txChargeId) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: txChargeId,
                  amount: "100",
                  currency: 'USD',
                  event_date: new Date('2024-01-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === docCharge1Id) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: docCharge1Id,
                  total_amount: 100,
                  currency_code: 'USD',
                  date: new Date('2024-01-15'),
                }),
              ]);
            }
            if (id === docCharge2Id) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: docCharge2Id,
                  total_amount: 100,
                  currency_code: 'USD',
                  date: new Date('2024-01-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

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

      // The transaction charge should find 2 identical doc matches and skip (ambiguous)
      // But the doc charges will also match each other with high confidence
      // Since processing is sequential, one doc will match the other doc
      // So we expect: 1 match (doc1-doc2), and tx skipped due to ambiguity
      expect(result.totalMatches).toBe(1);
      expect(result.skippedCharges).toHaveLength(1);
      expect(result.skippedCharges[0]).toBe(txChargeId);
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
    });

    it('should process multiple unmatched charges correctly', async () => {
      const tx1Id = 'tx-charge-1';
      const doc1Id = 'doc-charge-1';
      const tx2Id = 'tx-charge-2';
      const doc2Id = 'doc-charge-2';

      const mockChargesProvider = {
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(tx1Id, ADMIN_BUSINESS_ID),
            createCharge(doc1Id, ADMIN_BUSINESS_ID),
            createCharge(tx2Id, ADMIN_BUSINESS_ID),
            createCharge(doc2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === tx1Id) {
              return Promise.resolve([
                createMockTransaction({ charge_id: tx1Id, amount: "100", currency: 'USD' }),
              ]);
            }
            if (id === tx2Id) {
              return Promise.resolve([
                createMockTransaction({ charge_id: tx2Id, amount: "200", currency: 'EUR' }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === doc1Id) {
              return Promise.resolve([
                createMockDocument({ charge_id: doc1Id, total_amount: 100, currency_code: 'USD' }),
              ]);
            }
            if (id === doc2Id) {
              return Promise.resolve([
                createMockDocument({ charge_id: doc2Id, total_amount: 200, currency_code: 'EUR' }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

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

      expect(result.totalMatches).toBe(2);
      expect(result.mergedCharges).toHaveLength(2);
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
    });

    it('should capture errors during merge but continue processing', async () => {
      const tx1Id = 'tx-charge-1';
      const doc1Id = 'doc-charge-1';
      const tx2Id = 'tx-charge-2'; // This will have no match

      const mockChargesProvider = {
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(tx1Id, ADMIN_BUSINESS_ID),
            createCharge(doc1Id, ADMIN_BUSINESS_ID),
            createCharge(tx2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === tx1Id) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: id,
                  amount: "100",
                  currency: 'USD',
                  event_date: new Date('2024-01-15'),
                }),
              ]);
            }
            if (id === tx2Id) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: id,
                  amount: "999",
                  currency: 'GBP',
                  event_date: new Date('2024-03-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === doc1Id) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: id,
                  total_amount: 100,
                  currency_code: 'USD',
                  date: new Date('2024-01-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      // Mock merge to fail on first call, succeed on second
      (mergeChargesExecutor as any).mockImplementationOnce(() => {
        throw new Error('Merge failed for test');
      });
      (mergeChargesExecutor as any).mockImplementationOnce(() => Promise.resolve());

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

      // tx1 finds doc1, merge fails → error captured, charges not marked as merged
      // doc1 finds tx1, merge succeeds → totalMatches++, charges marked as merged
      // tx2 has no match → silent
      expect(result.totalMatches).toBe(1);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]).toContain('Failed to merge');
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
    });

    it('should verify merge direction keeps transaction charge', async () => {
      const txChargeId = 'tx-charge-1';
      const docChargeId = 'doc-charge-1';

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

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

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

      const provider = createMockProvider(mockChargesProvider, mockTransactionsProvider, mockDocumentsProvider);
      await provider.autoMatchCharges();

      // Verify merge was called with doc charge being merged into tx charge
      expect(mergeChargesExecutor).toHaveBeenCalledWith([docChargeId], txChargeId, expect.any(Object));
    });

    it('should exclude merged charges from further processing in same run', async () => {
      const tx1Id = 'tx-charge-1';
      const doc1Id = 'doc-charge-1';
      const tx2Id = 'tx-charge-2';

      const mockChargesProvider = {
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(tx1Id, ADMIN_BUSINESS_ID),
            createCharge(doc1Id, ADMIN_BUSINESS_ID),
            createCharge(tx2Id, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

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

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === doc1Id) {
              return Promise.resolve([createMockDocument({ charge_id: doc1Id, total_amount: 100 })]);
            }
            return Promise.resolve([]);
          }),
        },
      };

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

      // Should only merge tx1 with doc1, tx2 should have no match
      expect(result.totalMatches).toBe(1);
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(1);
    });

    it('should handle mixed scenarios: some match, some skip, some no-match', async () => {
      const perfectMatchTx = 'tx-perfect';
      const perfectMatchDoc = 'doc-perfect';
      const ambiguousTx = 'tx-ambiguous';
      const ambiguousDoc1 = 'doc-ambiguous-1';
      const ambiguousDoc2 = 'doc-ambiguous-2';
      const noMatchTx = 'tx-no-match';

      const mockChargesProvider = {
        getChargesByFilters: vi.fn(() =>
          Promise.resolve([
            createCharge(perfectMatchTx, ADMIN_BUSINESS_ID),
            createCharge(perfectMatchDoc, ADMIN_BUSINESS_ID),
            createCharge(ambiguousTx, ADMIN_BUSINESS_ID),
            createCharge(ambiguousDoc1, ADMIN_BUSINESS_ID),
            createCharge(ambiguousDoc2, ADMIN_BUSINESS_ID),
            createCharge(noMatchTx, ADMIN_BUSINESS_ID),
          ]),
        ),
      };

      const mockTransactionsProvider = {
        transactionsByChargeIDLoader: {
          load: vi.fn((id: string) => {
            if (id === perfectMatchTx) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: id,
                  amount: "100",
                  currency: 'USD',
                  event_date: new Date('2024-01-15'),
                }),
              ]);
            }
            if (id === ambiguousTx) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: id,
                  amount: "200",
                  currency: 'EUR',
                  event_date: new Date('2024-02-15'),
                }),
              ]);
            }
            if (id === noMatchTx) {
              return Promise.resolve([
                createMockTransaction({
                  charge_id: id,
                  amount: "500",
                  currency: 'GBP',
                  event_date: new Date('2024-03-15'),
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      const mockDocumentsProvider = {
        getDocumentsByChargeIdLoader: {
          load: vi.fn((id: string) => {
            if (id === perfectMatchDoc) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: id,
                  total_amount: 100,
                  currency_code: 'USD',
                  date: new Date('2024-01-15'),
                  debtor_id: ADMIN_BUSINESS_ID,
                  creditor_id: 'business-a',
                }),
              ]);
            }
            if (id === ambiguousDoc1) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: id,
                  total_amount: 200,
                  currency_code: 'EUR',
                  date: new Date('2024-02-15'),
                  vat_amount: 10, // Different VAT
                }),
              ]);
            }
            if (id === ambiguousDoc2) {
              return Promise.resolve([
                createMockDocument({
                  charge_id: id,
                  total_amount: 200,
                  currency_code: 'EUR',
                  date: new Date('2024-02-15'),
                  vat_amount: 20, // Different VAT - but this won't affect matching score
                }),
              ]);
            }
            return Promise.resolve([]);
          }),
        },
      };

      (mergeChargesExecutor as any).mockImplementation(() => Promise.resolve());

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

      // Expected results:
      // - perfectMatchTx finds perfectMatchDoc → merge (1)
      // - perfectMatchDoc → already merged, skip processing
      // - ambiguousTx finds 2 docs (ambiguousDoc1, ambiguousDoc2) → skipped due to ambiguity
      // - ambiguousDoc1 finds ambiguousDoc2 (tx was skipped, not merged) → merge (2)
      // - ambiguousDoc2 → already merged, skip processing
      // - noMatchTx → no match, silent
      // Total: 2 successful matches, 1 skipped charge (tx-ambiguous)
      expect(result.totalMatches).toBe(2);
      expect(result.mergedCharges).toHaveLength(2);
      expect(result.skippedCharges).toHaveLength(1);
      expect(result.skippedCharges[0]).toBe(ambiguousTx);
      expect(mergeChargesExecutor).toHaveBeenCalledTimes(2);
    });

    it('should throw error if admin business ID not found 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,
        { getChargesByFilters: vi.fn() } as any,
        { transactionsByChargeIDLoader: { load: vi.fn() } } as any,
        { getDocumentsByChargeIdLoader: { load: vi.fn() } } as any,
        { injector: {} } as any,
      );

      await expect(
        provider.autoMatchCharges(),
      ).rejects.toThrow(/Admin context not found/);
    });
  });
});
