// Imports
import {
	AuthProvider,
	Environment,
	ApiClient,
	AuthScopes,
	InvoiceOptions,
	InvoiceStatus,
	Invoice,
	CustomerOptions,
	QuickbooksError,
} from '../src/app';
import { describe, expect, test } from 'bun:test';

// Describe the Invoice API
describe('Live API: Invoices', async () => {
	// Initialize the Auth Provider
	const authProvider = new AuthProvider(
		process.env.QB_CLIENT_ID!,
		process.env.QB_CLIENT_SECRET!,
		process.env.REDIRECT_URI!,
		[AuthScopes.Accounting],
		null,
		Environment.Sandbox,
	);

	// Deserialize the Token
	await authProvider.deserializeToken(process.env.SERIALIZED_TOKEN!, process.env.SECRET_KEY!);

	// Setup the API Client
	const apiClient = new ApiClient(authProvider, Environment.Sandbox);

	// Test retrieving all invoices
	test('should retrieve all invoices', async () => {
		// Get all invoices
		const searchResponse = await apiClient.invoices.getAllInvoices();

		// Test the Invoices
		expect(searchResponse.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponse.results.length).toBeGreaterThan(0);

		// Test the Intuit TID
		expect(searchResponse.intuitTID).toBeDefined();
		expect(typeof searchResponse.intuitTID).toBe('string');
	});

	// Test Checking for Next Page
	test('should check for next page', async () => {
		// Get all invoices
		const searchResponse = await apiClient.invoices.getAllInvoices();

		// Test the Invoices
		expect(searchResponse.hasNextPage).toBe(true);

		// Test the Intuit TID
		expect(searchResponse.intuitTID).toBeDefined();
		expect(typeof searchResponse.intuitTID).toBe('string');
	});

	// Test retrieving a single invoice
	test('should retrieve a single invoice', async () => {
		// Get all invoices
		const searchResponse = await apiClient.invoices.getAllInvoices();

		// Get the first invoice
		const invoice = searchResponse.results[0];

		// Get the Invoice by ID
		const invoiceResponse = await apiClient.invoices.getInvoiceById(invoice.Id);

		// Test the Invoice Response Structure
		expect(invoiceResponse).toBeDefined();
		expect(invoiceResponse).toHaveProperty('invoice');
		expect(invoiceResponse).toHaveProperty('intuitTID');
		expect(typeof invoiceResponse.intuitTID).toBe('string');

		// Test the Invoice ID
		expect(invoiceResponse.invoice?.Id).toBe(invoice.Id);
	});

	// Test retrieving 10 invoices
	test('should retrieve 10 invoices', async () => {
		// Setup the Invoice Options
		const invoiceOptions: InvoiceOptions = { searchOptions: { maxResults: 10 } };

		// Get all invoices
		const searchResponse = await apiClient.invoices.getAllInvoices(invoiceOptions);

		// Test the Invoices
		expect(searchResponse.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponse.results.length).toBeGreaterThan(0);

		// Test the Intuit TID
		expect(searchResponse.intuitTID).toBeDefined();
		expect(typeof searchResponse.intuitTID).toBe('string');
	});

	// Test retrieving Invoices by Status
	test('should retrieve Invoices by Status', async () => {
		// Setup the Invoice Options
		const invoiceOptionsPaid: InvoiceOptions = { searchOptions: { maxResults: 10 }, status: InvoiceStatus.Paid };
		const invoiceOptionsUnpaid: InvoiceOptions = { searchOptions: { maxResults: 10 }, status: InvoiceStatus.Unpaid };

		// Get all invoices
		const searchResponsePaid = await apiClient.invoices.getAllInvoices(invoiceOptionsPaid);
		const searchResponseUnpaid = await apiClient.invoices.getAllInvoices(invoiceOptionsUnpaid);

		// Test the Invoices
		expect(searchResponsePaid.results).toBeInstanceOf(Array);
		expect(searchResponseUnpaid.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponsePaid.results.length).toBeGreaterThan(0);
		expect(searchResponseUnpaid.results.length).toBeGreaterThan(0);

		// Test the Invoices are different
		expect(searchResponsePaid.results).not.toEqual(searchResponseUnpaid.results);

		// Test the Invoices are paid
		await Promise.all(
			searchResponsePaid.results.map(async (invoice: Invoice) => {
				expect(invoice.Balance).toBe(0);
			}),
		);

		// Test the Invoices are unpaid
		await Promise.all(
			searchResponseUnpaid.results.map(async (invoice: Invoice) => {
				expect(invoice.Balance).toBeGreaterThan(0);
			}),
		);
	});

	// Test retrieving Invoices by Status
	test('should retrieve Invoices by Customer ID', async () => {
		// Setup the Customer Options
		const customerOptions: CustomerOptions = { searchOptions: { maxResults: 1 } };

		// Setup the Invoice Options
		const invoiceOptions: InvoiceOptions = { searchOptions: { maxResults: 10 } };

		// Get the Customer
		const customerResponse = await apiClient.customers.getAllCustomers(customerOptions);

		// Setup the Invoice Query Builder
		const invoiceQueryBuilder = await apiClient.invoices.getQueryBuilder();

		// Add the Customer ID Filter
		invoiceQueryBuilder.whereCustomerId(customerResponse.results[0].Id);

		// Add the Search Options
		invoiceQueryBuilder.setSearchOptions(invoiceOptions.searchOptions);

		// Make the Request
		const invoiceResponse = await apiClient.invoices.rawInvoiceQuery(invoiceQueryBuilder);

		// Test the Invoices
		expect(customerResponse.results).toBeInstanceOf(Array);
		expect(invoiceResponse.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(invoiceResponse.results.length).toBeGreaterThan(0);

		// Test the Invoices are for the Customer
		expect(invoiceResponse.results.every((invoice: Invoice) => invoice.CustomerRef.value === customerResponse.results[0].Id)).toBe(true);
	});

	// Test pagination
	test('should handle pagination', async () => {
		// Setup the Invoice Options
		const invoiceOptions1: InvoiceOptions = { searchOptions: { maxResults: 10, page: 1 } };
		const invoiceOptions2: InvoiceOptions = { searchOptions: { maxResults: 10, page: 2 } };

		// Get all invoices
		const searchResponse1 = await apiClient.invoices.getAllInvoices(invoiceOptions1);
		const searchResponse2 = await apiClient.invoices.getAllInvoices(invoiceOptions2);

		// Test the Invoices
		expect(searchResponse1.results).toBeInstanceOf(Array);
		expect(searchResponse2.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponse1.results.length).toBeGreaterThan(0);
		expect(searchResponse2.results.length).toBeGreaterThan(0);

		// Test the Invoices are different
		expect(searchResponse1.results).not.toEqual(searchResponse2.results);
	});

	// Should handle all Search Options
	test('should handle all search options', async () => {
		// Setup the Invoice Options
		const invoiceOptions: InvoiceOptions = {
			searchOptions: {
				maxResults: 10,
				page: 1,
				orderBy: { field: 'Id', direction: 'DESC' },
			},
		};

		// Get all invoices
		const searchResponse = await apiClient.invoices.getAllInvoices(invoiceOptions);

		// Test the Invoices
		expect(searchResponse.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponse.results.length).toBeGreaterThan(0);

		// Test the Intuit TID
		expect(searchResponse.intuitTID).toBeDefined();
		expect(typeof searchResponse.intuitTID).toBe('string');

		// loop through the invoices and test each id is less than the previous one
		for (let i = 0; i < searchResponse.results.length - 1; i++)
			expect(Number(searchResponse.results[i].Id)).toBeGreaterThan(Number(searchResponse.results[i + 1].Id));
	});

	// Test retrieving updated invoices
	test('should retrieve updated invoices', async () => {
		// Get the Last Updated Time
		const lastUpdatedTime = new Date('2012-01-08');

		// Get the Invoices
		const searchResponse = await apiClient.invoices.getUpdatedInvoices(lastUpdatedTime);

		// Test the Invoices
		expect(searchResponse.results).toBeInstanceOf(Array);

		// Test the Invoice length
		expect(searchResponse.results.length).toBeGreaterThan(0);

		// Test the Intuit TID
		expect(searchResponse.intuitTID).toBeDefined();
		expect(typeof searchResponse.intuitTID).toBe('string');
	});

	// Test returning an empty array if no invoices are updated
	test('should return an empty array if no invoices are updated', async () => {
		// Setup the Future Date
		const futureDate = new Date();

		// Set the New Full Year
		futureDate.setFullYear(futureDate.getFullYear() + 20);

		// Get the Invoices
		const searchResponse = await apiClient.invoices.getUpdatedInvoices(futureDate);

		// Assert the Invoices
		expect(searchResponse.results).toBeArray();

		// Assert the Invoices Length
		expect(searchResponse.results.length).toBe(0);
	});

	// Test error handling for invalid ID
	test('should throw QuickbooksError for invalid invoice ID', async () => {
		try {
			await apiClient.invoices.getInvoiceById('invalid');
			expect(false).toBe(true); // Should not reach here
		} catch (error) {
			// Assert the Error is a QuickbooksError
			expect(error).toBeInstanceOf(QuickbooksError);
			expect(error).toBeInstanceOf(Error);

			// Assert the Error has the correct structure
			expect(error.message).toBeDefined();
			expect(error.details).toBeDefined();
			expect(error.details.statusCode).toBeDefined();
			expect(typeof error.details.statusCode).toBe('number');
			expect(error.details.intuitError).toBeDefined();
			expect(Array.isArray(error.details.intuitError)).toBe(true);
			expect(error.details.intuitTID).toBeDefined();
			expect(typeof error.details.intuitTID).toBe('string');
		}
	});

	// Test error handling for invalid raw query
	test('should throw QuickbooksError for invalid raw query', async () => {
		// Get the Query Builder
		const queryBuilder = await apiClient.invoices.getQueryBuilder();

		// Add an invalid ID filter that will cause an error
		queryBuilder.whereId('invalid-id-that-does-not-exist');

		try {
			await apiClient.invoices.rawInvoiceQuery(queryBuilder);
			expect(false).toBe(true); // Should not reach here
		} catch (error) {
			// Assert the Error is a QuickbooksError
			expect(error).toBeInstanceOf(QuickbooksError);
			expect(error).toBeInstanceOf(Error);

			// Assert the Error has the correct structure
			expect(error.message).toBeDefined();
			expect(error.details).toBeDefined();
			expect(error.details.statusCode).toBeDefined();
			expect(typeof error.details.statusCode).toBe('number');
			expect(error.details.intuitError).toBeDefined();
			expect(Array.isArray(error.details.intuitError)).toBe(true);
			expect(error.details.intuitTID).toBeDefined();
			expect(typeof error.details.intuitTID).toBe('string');
		}
	});

	// Test Invoice class methods
	describe('Invoice Class Methods', () => {
		// Test downloading invoice PDF
		test('should download invoice PDF', async () => {
			// Get all invoices
			const searchResponse = await apiClient.invoices.getAllInvoices({ searchOptions: { maxResults: 1 } });

			// Check if we have at least one invoice
			if (searchResponse.results.length === 0) {
				console.log('No invoices found, skipping PDF download test');
				return;
			}

			// Get the first invoice
			const invoice = searchResponse.results[0];

			// Get the invoice by ID to get class instance
			const invoiceResponse = await apiClient.invoices.getInvoiceById(invoice.Id);

			// Check if invoice exists
			if (!invoiceResponse.invoice) {
				console.log('Invoice not found, skipping PDF download test');
				return;
			}

			// Download the PDF
			const pdf = await invoiceResponse.invoice.downloadPDF();

			// Assert the PDF is a Blob
			expect(pdf).toBeInstanceOf(Blob);
			expect(pdf.type).toContain('application/pdf');
			expect(pdf.size).toBeGreaterThan(0);
		});
	});
});
