// Mock dependencies
jest.mock("stripe", () => {
  const mockStripe = {
    payouts: {
      list: jest.fn(),
    },
    transfers: {
      retrieve: jest.fn(),
    },
    balanceTransactions: {
      list: jest.fn(),
    },
  };
  return jest.fn(() => mockStripe);
});

jest.mock("../../../models", () => ({
  Rental: {
    findAll: jest.fn(),
    update: jest.fn(),
  },
  User: {
    findOne: jest.fn(),
  },
  Item: jest.fn(),
}));

jest.mock("../../../services/payoutService", () => ({
  processRentalPayout: jest.fn(),
}));

jest.mock("../../../utils/logger", () => ({
  info: jest.fn(),
  warn: jest.fn(),
  error: jest.fn(),
}));

jest.mock("../../../services/email", () => ({
  payment: {
    sendPayoutFailedNotification: jest.fn(),
    sendAccountDisconnectedEmail: jest.fn(),
  },
}));

jest.mock("sequelize", () => ({
  Op: {
    not: "not",
    is: "is",
    in: "in",
  },
}));

const StripeWebhookService = require("../../../services/stripeWebhookService");
const { Rental, User } = require("../../../models");
const emailServices = require("../../../services/email");
const stripe = require("stripe")();

describe("StripeWebhookService", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe("reconcilePayoutStatuses", () => {
    const mockOwnerId = "owner-123";
    const mockConnectedAccountId = "acct_test123";

    it("should return early if no rentals need reconciliation", async () => {
      Rental.findAll.mockResolvedValue([]);

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result).toEqual({
        reconciled: 0,
        updated: 0,
        failed: 0,
        notificationsSent: 0,
        errors: [],
      });
      expect(stripe.payouts.list).not.toHaveBeenCalled();
    });

    it("should return early if owner has no connected account", async () => {
      Rental.findAll.mockResolvedValue([
        {
          id: 1,
          stripeTransferId: "tr_123",
          owner: { stripeConnectedAccountId: null },
        },
      ]);

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result).toEqual({
        reconciled: 0,
        updated: 0,
        failed: 0,
        notificationsSent: 0,
        errors: [],
      });
    });

    it("should reconcile rental with paid payout", async () => {
      const mockRental = {
        id: 1,
        stripeTransferId: "tr_123",
        owner: {
          id: "owner-123",
          email: "owner@test.com",
          firstName: "Test",
          stripeConnectedAccountId: mockConnectedAccountId,
        },
        update: jest.fn().mockResolvedValue(true),
      };

      Rental.findAll.mockResolvedValue([mockRental]);

      stripe.payouts.list.mockImplementation((params) => {
        if (params.status === "paid") {
          return Promise.resolve({
            data: [
              {
                id: "po_paid123",
                status: "paid",
                arrival_date: 1700000000,
              },
            ],
          });
        }
        return Promise.resolve({ data: [] });
      });

      stripe.balanceTransactions.list.mockResolvedValue({ data: [] });

      stripe.transfers.retrieve.mockResolvedValue({
        id: "tr_123",
        created: 1699900000, // Before arrival_date
      });

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result.updated).toBe(1);
      expect(result.failed).toBe(0);
      expect(mockRental.update).toHaveBeenCalledWith({
        bankDepositStatus: "paid",
        bankDepositAt: expect.any(Date),
        stripePayoutId: "po_paid123",
      });
    });

    it("should reconcile rental with failed payout and send notification", async () => {
      const mockRental = {
        id: 1,
        stripeTransferId: "tr_123",
        owner: {
          id: "owner-123",
          email: "owner@test.com",
          firstName: "Test",
          stripeConnectedAccountId: mockConnectedAccountId,
        },
        update: jest.fn().mockResolvedValue(true),
      };

      Rental.findAll.mockResolvedValue([mockRental]);

      stripe.payouts.list.mockImplementation((params) => {
        if (params.status === "failed") {
          return Promise.resolve({
            data: [
              {
                id: "po_failed123",
                status: "failed",
                failure_code: "account_closed",
                amount: 5000,
              },
            ],
          });
        }
        return Promise.resolve({ data: [] });
      });

      // Mock balance transactions showing the transfer is in the failed payout
      stripe.balanceTransactions.list.mockResolvedValue({
        data: [{ source: "tr_123" }],
      });

      emailServices.payment.sendPayoutFailedNotification.mockResolvedValue({
        success: true,
      });

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result.failed).toBe(1);
      expect(result.notificationsSent).toBe(1);
      expect(mockRental.update).toHaveBeenCalledWith({
        bankDepositStatus: "failed",
        stripePayoutId: "po_failed123",
        bankDepositFailureCode: "account_closed",
      });
      expect(
        emailServices.payment.sendPayoutFailedNotification
      ).toHaveBeenCalledWith("owner@test.com", {
        ownerName: "Test",
        payoutAmount: 50, // 5000 cents = $50
        failureMessage: "Your bank account has been closed.",
        actionRequired:
          "Please update your bank account in your payout settings.",
        failureCode: "account_closed",
        requiresBankUpdate: true,
      });
    });

    it("should handle multiple rentals with mixed statuses", async () => {
      const mockRentals = [
        {
          id: 1,
          stripeTransferId: "tr_failed",
          owner: {
            id: "owner-123",
            email: "owner@test.com",
            firstName: "Test",
            stripeConnectedAccountId: mockConnectedAccountId,
          },
          update: jest.fn().mockResolvedValue(true),
        },
        {
          id: 2,
          stripeTransferId: "tr_paid",
          owner: {
            id: "owner-123",
            email: "owner@test.com",
            firstName: "Test",
            stripeConnectedAccountId: mockConnectedAccountId,
          },
          update: jest.fn().mockResolvedValue(true),
        },
      ];

      Rental.findAll.mockResolvedValue(mockRentals);

      stripe.payouts.list.mockImplementation((params) => {
        if (params.status === "failed") {
          return Promise.resolve({
            data: [
              {
                id: "po_failed",
                status: "failed",
                failure_code: "account_frozen",
                amount: 3000,
              },
            ],
          });
        }
        if (params.status === "paid") {
          return Promise.resolve({
            data: [
              {
                id: "po_paid",
                status: "paid",
                arrival_date: 1700000000,
              },
            ],
          });
        }
        return Promise.resolve({ data: [] });
      });

      // tr_failed is in the failed payout
      stripe.balanceTransactions.list.mockResolvedValue({
        data: [{ source: "tr_failed" }],
      });

      stripe.transfers.retrieve.mockResolvedValue({
        id: "tr_paid",
        created: 1699900000,
      });

      emailServices.payment.sendPayoutFailedNotification.mockResolvedValue({
        success: true,
      });

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result.reconciled).toBe(2);
      expect(result.failed).toBe(1);
      expect(result.updated).toBe(1);
      expect(result.notificationsSent).toBe(1);
    });

    it("should continue processing other rentals when one fails", async () => {
      const mockRentals = [
        {
          id: 1,
          stripeTransferId: "tr_error",
          owner: {
            id: "owner-123",
            email: "owner@test.com",
            firstName: "Test",
            stripeConnectedAccountId: mockConnectedAccountId,
          },
          update: jest.fn().mockRejectedValue(new Error("DB error")),
        },
        {
          id: 2,
          stripeTransferId: "tr_success",
          owner: {
            id: "owner-123",
            email: "owner@test.com",
            firstName: "Test",
            stripeConnectedAccountId: mockConnectedAccountId,
          },
          update: jest.fn().mockResolvedValue(true),
        },
      ];

      Rental.findAll.mockResolvedValue(mockRentals);

      stripe.payouts.list.mockImplementation((params) => {
        if (params.status === "paid") {
          return Promise.resolve({
            data: [{ id: "po_paid", status: "paid", arrival_date: 1700000000 }],
          });
        }
        return Promise.resolve({ data: [] });
      });

      stripe.balanceTransactions.list.mockResolvedValue({ data: [] });

      stripe.transfers.retrieve.mockResolvedValue({
        created: 1699900000,
      });

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      expect(result.reconciled).toBe(2);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].rentalId).toBe(1);
      expect(result.updated).toBe(1);
    });

    it("should handle email notification failure gracefully", async () => {
      const mockRental = {
        id: 1,
        stripeTransferId: "tr_123",
        owner: {
          id: "owner-123",
          email: "owner@test.com",
          firstName: "Test",
          stripeConnectedAccountId: mockConnectedAccountId,
        },
        update: jest.fn().mockResolvedValue(true),
      };

      Rental.findAll.mockResolvedValue([mockRental]);

      stripe.payouts.list.mockImplementation((params) => {
        if (params.status === "failed") {
          return Promise.resolve({
            data: [
              {
                id: "po_failed",
                failure_code: "declined",
                amount: 1000,
              },
            ],
          });
        }
        return Promise.resolve({ data: [] });
      });

      stripe.balanceTransactions.list.mockResolvedValue({
        data: [{ source: "tr_123" }],
      });

      emailServices.payment.sendPayoutFailedNotification.mockRejectedValue(
        new Error("Email service down")
      );

      const result =
        await StripeWebhookService.reconcilePayoutStatuses(mockOwnerId);

      // Should still mark as failed even if notification fails
      expect(result.failed).toBe(1);
      expect(result.notificationsSent).toBe(0);
      expect(mockRental.update).toHaveBeenCalled();
    });
  });

  describe("handleAccountDeauthorized", () => {
    it("should return missing_account_id when accountId is not provided", async () => {
      const result = await StripeWebhookService.handleAccountDeauthorized(null);

      expect(result.processed).toBe(false);
      expect(result.reason).toBe("missing_account_id");
    });

    it("should return missing_account_id for undefined accountId", async () => {
      const result = await StripeWebhookService.handleAccountDeauthorized(undefined);

      expect(result.processed).toBe(false);
      expect(result.reason).toBe("missing_account_id");
    });

    it("should return user_not_found when account does not match any user", async () => {
      User.findOne.mockResolvedValue(null);

      const result = await StripeWebhookService.handleAccountDeauthorized("acct_unknown");

      expect(result.processed).toBe(false);
      expect(result.reason).toBe("user_not_found");
      expect(User.findOne).toHaveBeenCalledWith({
        where: { stripeConnectedAccountId: "acct_unknown" },
      });
    });

    it("should clear Stripe connection fields when account is deauthorized", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Test",
        name: "Test Owner",
        stripeConnectedAccountId: "acct_123",
        stripePayoutsEnabled: true,
        update: jest.fn().mockResolvedValue(true),
      };

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue([]);
      emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
        success: true,
      });

      const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");

      expect(result.processed).toBe(true);
      expect(result.userId).toBe(1);
      expect(mockUser.update).toHaveBeenCalledWith({
        stripeConnectedAccountId: null,
        stripePayoutsEnabled: false,
      });
    });

    it("should log warning when there are pending payouts", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Test",
        update: jest.fn().mockResolvedValue(true),
      };
      const mockRentals = [{ id: 100 }, { id: 101 }];

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue(mockRentals);
      emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
        success: true,
      });

      const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");

      expect(result.pendingPayoutsCount).toBe(2);
      expect(Rental.findAll).toHaveBeenCalledWith({
        where: {
          ownerId: 1,
          payoutStatus: "pending",
        },
      });
    });

    it("should send notification email to owner", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Test",
        update: jest.fn().mockResolvedValue(true),
      };

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue([]);
      emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
        success: true,
      });

      const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");

      expect(result.notificationSent).toBe(true);
      expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
        "owner@test.com",
        {
          ownerName: "Test",
          hasPendingPayouts: false,
          pendingPayoutCount: 0,
        }
      );
    });

    it("should send notification with pending payout info when there are pending payouts", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Owner",
        update: jest.fn().mockResolvedValue(true),
      };
      const mockRentals = [{ id: 100 }, { id: 101 }, { id: 102 }];

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue(mockRentals);
      emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
        success: true,
      });

      await StripeWebhookService.handleAccountDeauthorized("acct_123");

      expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
        "owner@test.com",
        {
          ownerName: "Owner",
          hasPendingPayouts: true,
          pendingPayoutCount: 3,
        }
      );
    });

    it("should use name fallback when firstName is not available", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: null,
        name: "Full Name",
        update: jest.fn().mockResolvedValue(true),
      };

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue([]);
      emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
        success: true,
      });

      await StripeWebhookService.handleAccountDeauthorized("acct_123");

      expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
        "owner@test.com",
        expect.objectContaining({
          ownerName: "Full Name",
        })
      );
    });

    it("should handle email sending failure gracefully", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Test",
        update: jest.fn().mockResolvedValue(true),
      };

      User.findOne.mockResolvedValue(mockUser);
      Rental.findAll.mockResolvedValue([]);
      emailServices.payment.sendAccountDisconnectedEmail.mockRejectedValue(
        new Error("Email service down")
      );

      const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");

      // Should still mark as processed even if notification fails
      expect(result.processed).toBe(true);
      expect(result.notificationSent).toBe(false);
      expect(mockUser.update).toHaveBeenCalled();
    });

    it("should throw error when database update fails", async () => {
      const mockUser = {
        id: 1,
        email: "owner@test.com",
        firstName: "Test",
        update: jest.fn().mockRejectedValue(new Error("DB error")),
      };

      User.findOne.mockResolvedValue(mockUser);

      await expect(
        StripeWebhookService.handleAccountDeauthorized("acct_123")
      ).rejects.toThrow("DB error");
    });
  });
});
