/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {Config} from '@fluxer/api/src/Config';
import {DonationRepository} from '@fluxer/api/src/donation/DonationRepository';
import {DonorMagicLinkToken} from '@fluxer/api/src/donation/models/DonorMagicLinkToken';
import {
	createDonationManageBuilder,
	TEST_DONOR_EMAIL,
	TEST_INVALID_TOKEN,
	TEST_MAGIC_LINK_TOKEN,
} from '@fluxer/api/src/donation/tests/DonationTestUtils';
import {createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {createPwnedPasswordsRangeHandler} from '@fluxer/api/src/test/msw/handlers/PwnedPasswordsHandlers';
import {createStripeApiHandlers} from '@fluxer/api/src/test/msw/handlers/StripeApiHandlers';
import {server} from '@fluxer/api/src/test/msw/server';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest';

describe('GET /donations/manage', () => {
	let harness: Awaited<ReturnType<typeof createApiTestHarness>>;
	let stripeHandlers: ReturnType<typeof createStripeApiHandlers>;
	let donationRepository: DonationRepository;

	beforeAll(async () => {
		harness = await createApiTestHarness();
		donationRepository = new DonationRepository();
		stripeHandlers = createStripeApiHandlers();
	});

	afterAll(async () => {
		await harness.shutdown();
	});

	beforeEach(async () => {
		await harness.reset();
		stripeHandlers.reset();
		server.use(...stripeHandlers.handlers);
	});

	async function createDonorWithCustomerId(email: string, customerId: string): Promise<void> {
		await donationRepository.createDonor({
			email,
			stripeCustomerId: customerId,
			businessName: null,
			taxId: null,
			taxIdType: null,
			stripeSubscriptionId: 'sub_test_1',
			subscriptionAmountCents: 2500,
			subscriptionCurrency: 'usd',
			subscriptionInterval: 'month',
			subscriptionCurrentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
		});
	}

	async function createValidMagicLinkToken(email: string, token: string): Promise<void> {
		const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
		const tokenModel = new DonorMagicLinkToken({
			token_: token,
			donor_email: email,
			expires_at: expiresAt,
			used_at: null,
		});
		await donationRepository.createMagicLinkToken(tokenModel);
	}

	async function createExpiredMagicLinkToken(email: string, token: string): Promise<void> {
		const expiresAt = new Date(Date.now() - 1000);
		const tokenModel = new DonorMagicLinkToken({
			token_: token,
			donor_email: email,
			expires_at: expiresAt,
			used_at: null,
		});
		await donationRepository.createMagicLinkToken(tokenModel);
	}

	async function createUsedMagicLinkToken(email: string, token: string): Promise<void> {
		const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
		const usedAt = new Date(Date.now() - 5000);
		const tokenModel = new DonorMagicLinkToken({
			token_: token,
			donor_email: email,
			expires_at: expiresAt,
			used_at: usedAt,
		});
		await donationRepository.createMagicLinkToken(tokenModel);
	}

	describe('valid token with customer ID', () => {
		test('redirects to Stripe billing portal', async () => {
			const customerId = 'cus_test_valid_123';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			const {response} = await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(response.status).toBe(302);
			expect(response.headers.get('location')).toContain('https://billing.stripe.com');
			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(1);
			expect(stripeHandlers.spies.createdPortalSessions[0]?.customer).toBe(customerId);
		});

		test('marks token as used after validation', async () => {
			const customerId = 'cus_test_valid_456';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			const token = await donationRepository.findMagicLinkToken(TEST_MAGIC_LINK_TOKEN);
			expect(token?.isUsed()).toBe(true);
		});

		test('includes return_url in portal session', async () => {
			const customerId = 'cus_test_return_url';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(1);
			const portalSession = stripeHandlers.spies.createdPortalSessions[0];
			expect(portalSession?.return_url).toBeDefined();
		});
	});

	describe('valid token without customer ID', () => {
		test('redirects to donation page when donor has no customer ID', async () => {
			await donationRepository.createDonor({
				email: TEST_DONOR_EMAIL,
				stripeCustomerId: null,
				businessName: null,
				taxId: null,
				taxIdType: null,
				stripeSubscriptionId: null,
				subscriptionAmountCents: null,
				subscriptionCurrency: null,
				subscriptionInterval: null,
				subscriptionCurrentPeriodEnd: null,
			});
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			const {response} = await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(response.status).toBe(302);
			const location = response.headers.get('location');
			expect(location).toContain(Config.endpoints.marketing);
			expect(location).toContain('/donate');
			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('redirects to donation page when donor does not exist', async () => {
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			const {response} = await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(response.status).toBe(302);
			const location = response.headers.get('location');
			expect(location).toContain(Config.endpoints.marketing);
			expect(location).toContain('/donate');
			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('marks token as used even when redirecting to donate page', async () => {
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			const token = await donationRepository.findMagicLinkToken(TEST_MAGIC_LINK_TOKEN);
			expect(token?.isUsed()).toBe(true);
		});
	});

	describe('expired token handling', () => {
		test('rejects expired token', async () => {
			const customerId = 'cus_test_expired';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createExpiredMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_EXPIRED)
				.execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('does not mark expired token as used', async () => {
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, 'cus_test');
			await createExpiredMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_EXPIRED)
				.execute();

			const token = await donationRepository.findMagicLinkToken(TEST_MAGIC_LINK_TOKEN);
			expect(token?.isUsed()).toBe(false);
		});

		test('rejects token expired by exactly 1 millisecond', async () => {
			const expiresAt = new Date(Date.now() - 1);
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, 'cus_test');
			const tokenModel = new DonorMagicLinkToken({
				token_: TEST_MAGIC_LINK_TOKEN,
				donor_email: TEST_DONOR_EMAIL,
				expires_at: expiresAt,
				used_at: null,
			});
			await donationRepository.createMagicLinkToken(tokenModel);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_EXPIRED)
				.execute();
		});
	});

	describe('used token handling', () => {
		test('rejects already-used token', async () => {
			const customerId = 'cus_test_used';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createUsedMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_USED)
				.execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('prevents reuse of token after successful redemption', async () => {
			const customerId = 'cus_test_reuse';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_USED)
				.execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(1);
		});
	});

	describe('invalid token handling', () => {
		test('rejects non-existent token', async () => {
			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_INVALID)
				.execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token with invalid format', async () => {
			await createDonationManageBuilder(harness, TEST_INVALID_TOKEN).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects empty token', async () => {
			await createDonationManageBuilder(harness, '').expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token that is too short', async () => {
			const shortToken = 'a'.repeat(63);
			await createDonationManageBuilder(harness, shortToken).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token that is too long', async () => {
			const longToken = 'a'.repeat(65);
			await createDonationManageBuilder(harness, longToken).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token with non-hex characters', async () => {
			const invalidToken = 'g'.repeat(64);
			await createDonationManageBuilder(harness, invalidToken).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token with special characters', async () => {
			const specialToken = `${'a'.repeat(62)}@!`;
			await createDonationManageBuilder(harness, specialToken).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});

		test('rejects token with whitespace', async () => {
			const whitespaceToken = `${'a'.repeat(32)} ${'a'.repeat(31)}`;
			await createDonationManageBuilder(harness, whitespaceToken).expect(400).execute();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});
	});

	describe('missing token parameter', () => {
		test('rejects request without token query parameter', async () => {
			const response = await harness.requestJson({
				path: '/donations/manage',
				method: 'GET',
			});

			expect(response.status).toBe(400);
			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(0);
		});
	});

	describe('Stripe portal session creation', () => {
		test('handles Stripe API failure gracefully', async () => {
			const customerId = 'cus_test_stripe_fail';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			server.resetHandlers();
			const failingHandlers = createStripeApiHandlers({portalShouldFail: true});
			server.use(createPwnedPasswordsRangeHandler(), ...failingHandlers.handlers);

			const {response} = await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(response.status).toBe(400);
		});

		test('creates portal session with correct customer ID', async () => {
			const customerId = 'cus_test_specific_123';
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, customerId);
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(1);
			const session = stripeHandlers.spies.createdPortalSessions[0];
			expect(session?.customer).toBe(customerId);
		});
	});

	describe('edge cases', () => {
		test('handles token for donor with cancelled subscription', async () => {
			await donationRepository.createDonor({
				email: TEST_DONOR_EMAIL,
				stripeCustomerId: 'cus_test_cancelled',
				businessName: null,
				taxId: null,
				taxIdType: null,
				stripeSubscriptionId: null,
				subscriptionAmountCents: null,
				subscriptionCurrency: null,
				subscriptionInterval: null,
				subscriptionCurrentPeriodEnd: null,
			});
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, TEST_MAGIC_LINK_TOKEN);

			const {response} = await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN).executeRaw();

			expect(response.status).toBe(302);
			expect(response.headers.get('location')).toContain('https://billing.stripe.com');
			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(1);
		});

		test('handles token expiring in exactly 0 milliseconds', async () => {
			const expiresAt = new Date(Date.now());
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, 'cus_test');
			const tokenModel = new DonorMagicLinkToken({
				token_: TEST_MAGIC_LINK_TOKEN,
				donor_email: TEST_DONOR_EMAIL,
				expires_at: expiresAt,
				used_at: null,
			});
			await donationRepository.createMagicLinkToken(tokenModel);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_EXPIRED)
				.execute();
		});

		test('handles case-sensitive token comparison', async () => {
			const lowerToken = 'a'.repeat(64);
			const upperToken = 'A'.repeat(64);
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, 'cus_test');
			await createValidMagicLinkToken(TEST_DONOR_EMAIL, lowerToken);

			await createDonationManageBuilder(harness, upperToken)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_INVALID)
				.execute();
		});

		test('handles multiple valid tokens for different donors', async () => {
			const token1 = 'a'.repeat(64);
			const token2 = 'b'.repeat(64);
			const email1 = 'donor1@test.com';
			const email2 = 'donor2@test.com';
			const customerId1 = 'cus_test_1';
			const customerId2 = 'cus_test_2';

			await createDonorWithCustomerId(email1, customerId1);
			await createDonorWithCustomerId(email2, customerId2);
			await createValidMagicLinkToken(email1, token1);
			await createValidMagicLinkToken(email2, token2);

			const {response: response1} = await createDonationManageBuilder(harness, token1).executeRaw();
			expect(response1.status).toBe(302);
			expect(stripeHandlers.spies.createdPortalSessions[0]?.customer).toBe(customerId1);

			const {response: response2} = await createDonationManageBuilder(harness, token2).executeRaw();
			expect(response2.status).toBe(302);
			expect(stripeHandlers.spies.createdPortalSessions[1]?.customer).toBe(customerId2);

			expect(stripeHandlers.spies.createdPortalSessions).toHaveLength(2);
		});
	});

	describe('token validation order', () => {
		test('checks token existence before expiration', async () => {
			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_INVALID)
				.execute();
		});

		test('checks expiration before usage status', async () => {
			const expiresAt = new Date(Date.now() - 1000);
			const usedAt = new Date(Date.now() - 5000);
			await createDonorWithCustomerId(TEST_DONOR_EMAIL, 'cus_test');
			const tokenModel = new DonorMagicLinkToken({
				token_: TEST_MAGIC_LINK_TOKEN,
				donor_email: TEST_DONOR_EMAIL,
				expires_at: expiresAt,
				used_at: usedAt,
			});
			await donationRepository.createMagicLinkToken(tokenModel);

			await createDonationManageBuilder(harness, TEST_MAGIC_LINK_TOKEN)
				.expect(400, APIErrorCodes.DONATION_MAGIC_LINK_EXPIRED)
				.execute();
		});
	});
});
