const request = require("supertest");
const express = require("express");
const jwt = require("jsonwebtoken");

// Mock dependencies
jest.mock("jsonwebtoken");
jest.mock("../../../models", () => ({
  User: {
    findByPk: jest.fn(),
    create: jest.fn(),
    findOne: jest.fn(),
  },
  Item: {},
}));

jest.mock("../../../services/stripeService", () => ({
  getCheckoutSession: jest.fn(),
  createConnectedAccount: jest.fn(),
  createAccountLink: jest.fn(),
  getAccountStatus: jest.fn(),
  createCustomer: jest.fn(),
  createSetupCheckoutSession: jest.fn(),
}));

// Mock auth middleware
jest.mock("../../../middleware/auth", () => ({
  authenticateToken: (req, res, next) => {
    // Mock authenticated user
    if (req.headers.authorization) {
      req.user = { id: 1 };
      next();
    } else {
      res.status(401).json({ error: "No token provided" });
    }
  },
  requireVerifiedEmail: (req, res, next) => next(),
}));

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

const { User } = require("../../../models");
const StripeService = require("../../../services/stripeService");
const stripeRoutes = require("../../../routes/stripe");

// Set up Express app for testing
const app = express();
app.use(express.json());
app.use("/stripe", stripeRoutes);

// Error handler middleware
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

describe("Stripe Routes", () => {
  let consoleSpy, consoleErrorSpy;

  beforeEach(() => {
    jest.clearAllMocks();

    // Set up console spies
    consoleSpy = jest.spyOn(console, "log").mockImplementation();
    consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });

  describe("GET /checkout-session/:sessionId", () => {
    it("should retrieve checkout session successfully", async () => {
      const mockSession = {
        status: "complete",
        payment_status: "paid",
        customer_details: {
          email: "test@example.com",
        },
        setup_intent: {
          id: "seti_123456789",
          status: "succeeded",
        },
        metadata: {
          userId: "1",
        },
      };

      StripeService.getCheckoutSession.mockResolvedValue(mockSession);

      const response = await request(app).get(
        "/stripe/checkout-session/cs_123456789"
      );

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        status: "complete",
        payment_status: "paid",
        customer_email: "test@example.com",
        setup_intent: {
          id: "seti_123456789",
          status: "succeeded",
        },
        metadata: {
          userId: "1",
        },
      });

      expect(StripeService.getCheckoutSession).toHaveBeenCalledWith(
        "cs_123456789"
      );
    });

    it("should handle missing customer_details gracefully", async () => {
      const mockSession = {
        status: "complete",
        payment_status: "paid",
        customer_details: null,
        setup_intent: null,
        metadata: {},
      };

      StripeService.getCheckoutSession.mockResolvedValue(mockSession);

      const response = await request(app).get(
        "/stripe/checkout-session/cs_123456789"
      );

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        status: "complete",
        payment_status: "paid",
        customer_email: undefined,
        setup_intent: null,
        metadata: {},
      });
    });

    it("should handle checkout session retrieval errors", async () => {
      const error = new Error("Session not found");
      StripeService.getCheckoutSession.mockRejectedValue(error);

      const response = await request(app).get(
        "/stripe/checkout-session/invalid_session"
      );

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Session not found" });
    });

    it("should handle missing session ID", async () => {
      const error = new Error("Invalid session ID");
      StripeService.getCheckoutSession.mockRejectedValue(error);

      const response = await request(app).get("/stripe/checkout-session/");

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

  describe("POST /accounts", () => {
    const mockUser = {
      id: 1,
      email: "test@example.com",
      stripeConnectedAccountId: null,
      update: jest.fn(),
    };

    beforeEach(() => {
      mockUser.update.mockReset();
      mockUser.stripeConnectedAccountId = null;
    });

    it("should create connected account successfully", async () => {
      const mockAccount = {
        id: "acct_123456789",
        email: "test@example.com",
        country: "US",
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createConnectedAccount.mockResolvedValue(mockAccount);
      mockUser.update.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/accounts")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        stripeConnectedAccountId: "acct_123456789",
        success: true,
      });

      expect(User.findByPk).toHaveBeenCalledWith(1);
      expect(StripeService.createConnectedAccount).toHaveBeenCalledWith({
        email: "test@example.com",
        country: "US",
      });
      expect(mockUser.update).toHaveBeenCalledWith({
        stripeConnectedAccountId: "acct_123456789",
      });
    });

    it("should return error if user not found", async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post("/stripe/accounts")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: "User not found" });
      expect(StripeService.createConnectedAccount).not.toHaveBeenCalled();
    });

    it("should return error if user already has connected account", async () => {
      const userWithAccount = {
        ...mockUser,
        stripeConnectedAccountId: "acct_existing",
      };

      User.findByPk.mockResolvedValue(userWithAccount);

      const response = await request(app)
        .post("/stripe/accounts")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(400);
      expect(response.body).toEqual({
        error: "User already has a connected account",
      });
      expect(StripeService.createConnectedAccount).not.toHaveBeenCalled();
    });

    it("should require authentication", async () => {
      const response = await request(app).post("/stripe/accounts");

      expect(response.status).toBe(401);
      expect(response.body).toEqual({ error: "No token provided" });
    });

    it("should handle Stripe account creation errors", async () => {
      const error = new Error("Invalid email address");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createConnectedAccount.mockRejectedValue(error);

      const response = await request(app)
        .post("/stripe/accounts")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Invalid email address" });
      // Note: route uses logger instead of console.error
    });

    it("should handle database update errors", async () => {
      const mockAccount = { id: "acct_123456789" };
      const dbError = new Error("Database update failed");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createConnectedAccount.mockResolvedValue(mockAccount);
      mockUser.update.mockRejectedValue(dbError);

      const response = await request(app)
        .post("/stripe/accounts")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Database update failed" });
    });
  });

  describe("POST /account-links", () => {
    const mockUser = {
      id: 1,
      stripeConnectedAccountId: "acct_123456789",
    };

    it("should create account link successfully", async () => {
      const mockAccountLink = {
        url: "https://connect.stripe.com/setup/e/acct_123456789",
        expires_at: Date.now() + 3600,
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createAccountLink.mockResolvedValue(mockAccountLink);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          refreshUrl: "http://localhost:3000/refresh",
          returnUrl: "http://localhost:3000/return",
        });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        url: mockAccountLink.url,
        expiresAt: mockAccountLink.expires_at,
      });

      expect(StripeService.createAccountLink).toHaveBeenCalledWith(
        "acct_123456789",
        "http://localhost:3000/refresh",
        "http://localhost:3000/return"
      );
    });

    it("should return error if no connected account found", async () => {
      const userWithoutAccount = {
        id: 1,
        stripeConnectedAccountId: null,
      };

      User.findByPk.mockResolvedValue(userWithoutAccount);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          refreshUrl: "http://localhost:3000/refresh",
          returnUrl: "http://localhost:3000/return",
        });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: "No connected account found" });
      expect(StripeService.createAccountLink).not.toHaveBeenCalled();
    });

    it("should return error if user not found", async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          refreshUrl: "http://localhost:3000/refresh",
          returnUrl: "http://localhost:3000/return",
        });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: "No connected account found" });
    });

    it("should validate required URLs", async () => {
      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          refreshUrl: "http://localhost:3000/refresh",
          // Missing returnUrl
        });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({
        error: "refreshUrl and returnUrl are required",
      });
      expect(StripeService.createAccountLink).not.toHaveBeenCalled();
    });

    it("should validate both URLs are provided", async () => {
      User.findByPk.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          returnUrl: "http://localhost:3000/return",
          // Missing refreshUrl
        });

      expect(response.status).toBe(400);
      expect(response.body).toEqual({
        error: "refreshUrl and returnUrl are required",
      });
    });

    it("should require authentication", async () => {
      const response = await request(app).post("/stripe/account-links").send({
        refreshUrl: "http://localhost:3000/refresh",
        returnUrl: "http://localhost:3000/return",
      });

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

    it("should handle Stripe account link creation errors", async () => {
      const error = new Error("Account not found");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createAccountLink.mockRejectedValue(error);

      const response = await request(app)
        .post("/stripe/account-links")
        .set("Authorization", "Bearer valid_token")
        .send({
          refreshUrl: "http://localhost:3000/refresh",
          returnUrl: "http://localhost:3000/return",
        });

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Account not found" });
      // Note: route uses logger instead of console.error
    });
  });

  describe("GET /account-status", () => {
    const mockUser = {
      id: 1,
      stripeConnectedAccountId: "acct_123456789",
    };

    it("should get account status successfully", async () => {
      const mockAccountStatus = {
        id: "acct_123456789",
        details_submitted: true,
        payouts_enabled: true,
        capabilities: {
          transfers: { status: "active" },
        },
        requirements: {
          pending_verification: [],
          currently_due: [],
          past_due: [],
        },
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.getAccountStatus.mockResolvedValue(mockAccountStatus);

      const response = await request(app)
        .get("/stripe/account-status")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        accountId: "acct_123456789",
        detailsSubmitted: true,
        payoutsEnabled: true,
        capabilities: {
          transfers: { status: "active" },
        },
        requirements: {
          pending_verification: [],
          currently_due: [],
          past_due: [],
        },
      });

      expect(StripeService.getAccountStatus).toHaveBeenCalledWith(
        "acct_123456789"
      );
    });

    it("should return error if no connected account found", async () => {
      const userWithoutAccount = {
        id: 1,
        stripeConnectedAccountId: null,
      };

      User.findByPk.mockResolvedValue(userWithoutAccount);

      const response = await request(app)
        .get("/stripe/account-status")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: "No connected account found" });
      expect(StripeService.getAccountStatus).not.toHaveBeenCalled();
    });

    it("should return error if user not found", async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .get("/stripe/account-status")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(400);
      expect(response.body).toEqual({ error: "No connected account found" });
    });

    it("should require authentication", async () => {
      const response = await request(app).get("/stripe/account-status");

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

    it("should handle Stripe account status retrieval errors", async () => {
      const error = new Error("Account not found");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.getAccountStatus.mockRejectedValue(error);

      const response = await request(app)
        .get("/stripe/account-status")
        .set("Authorization", "Bearer valid_token");

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Account not found" });
      // Note: route uses logger instead of console.error
    });
  });

  describe("POST /create-setup-checkout-session", () => {
    const mockUser = {
      id: 1,
      email: "test@example.com",
      firstName: "John",
      lastName: "Doe",
      stripeCustomerId: null,
      update: jest.fn(),
    };

    beforeEach(() => {
      mockUser.update.mockReset();
      mockUser.stripeCustomerId = null;
    });

    it("should create setup checkout session for new customer", async () => {
      const mockCustomer = {
        id: "cus_123456789",
        email: "test@example.com",
      };

      const mockSession = {
        id: "cs_123456789",
        client_secret: "cs_123456789_secret_test",
      };

      const rentalData = {
        itemId: "123",
        startDate: "2023-12-01",
        endDate: "2023-12-03",
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockResolvedValue(mockCustomer);
      StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
      mockUser.update.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({ rentalData });

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        clientSecret: "cs_123456789_secret_test",
        sessionId: "cs_123456789",
      });

      expect(User.findByPk).toHaveBeenCalledWith(1);
      expect(StripeService.createCustomer).toHaveBeenCalledWith({
        email: "test@example.com",
        name: "John Doe",
        metadata: {
          userId: "1",
        },
      });
      expect(mockUser.update).toHaveBeenCalledWith({
        stripeCustomerId: "cus_123456789",
      });
      expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
        customerId: "cus_123456789",
        metadata: {
          rentalData: JSON.stringify(rentalData),
        },
      });
    });

    it("should use existing customer ID if available", async () => {
      const userWithCustomer = {
        ...mockUser,
        stripeCustomerId: "cus_existing123",
      };

      const mockSession = {
        id: "cs_123456789",
        client_secret: "cs_123456789_secret_test",
      };

      User.findByPk.mockResolvedValue(userWithCustomer);
      StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(200);
      expect(response.body).toEqual({
        clientSecret: "cs_123456789_secret_test",
        sessionId: "cs_123456789",
      });

      expect(StripeService.createCustomer).not.toHaveBeenCalled();
      expect(userWithCustomer.update).not.toHaveBeenCalled();
      expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
        customerId: "cus_existing123",
        metadata: {},
      });
    });

    it("should handle session without rental data", async () => {
      const mockCustomer = {
        id: "cus_123456789",
      };

      const mockSession = {
        id: "cs_123456789",
        client_secret: "cs_123456789_secret_test",
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockResolvedValue(mockCustomer);
      StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
      mockUser.update.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(200);
      expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
        customerId: "cus_123456789",
        metadata: {},
      });
    });

    it("should return error if user not found", async () => {
      User.findByPk.mockResolvedValue(null);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(404);
      expect(response.body).toEqual({ error: "User not found" });
      expect(StripeService.createCustomer).not.toHaveBeenCalled();
      expect(StripeService.createSetupCheckoutSession).not.toHaveBeenCalled();
    });

    it("should require authentication", async () => {
      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .send({});

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

    it("should handle customer creation errors", async () => {
      const error = new Error("Invalid email address");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockRejectedValue(error);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Invalid email address" });
      // Note: route uses logger.withRequestId().error() instead of console.error
    });

    it("should handle database update errors", async () => {
      const mockCustomer = { id: "cus_123456789" };
      const dbError = new Error("Database update failed");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockResolvedValue(mockCustomer);
      mockUser.update.mockRejectedValue(dbError);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Database update failed" });
    });

    it("should handle session creation errors", async () => {
      const mockCustomer = { id: "cus_123456789" };
      const sessionError = new Error("Session creation failed");

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockResolvedValue(mockCustomer);
      mockUser.update.mockResolvedValue(mockUser);
      StripeService.createSetupCheckoutSession.mockRejectedValue(sessionError);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({});

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Session creation failed" });
    });

    it("should handle complex rental data", async () => {
      const mockCustomer = { id: "cus_123456789" };
      const mockSession = {
        id: "cs_123456789",
        client_secret: "cs_123456789_secret_test",
      };

      const complexRentalData = {
        itemId: "123",
        startDate: "2023-12-01",
        endDate: "2023-12-03",
        totalAmount: 150.0,
        additionalServices: ["cleaning", "delivery"],
        notes: "Special instructions",
      };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createCustomer.mockResolvedValue(mockCustomer);
      StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
      mockUser.update.mockResolvedValue(mockUser);

      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .send({ rentalData: complexRentalData });

      expect(response.status).toBe(200);
      expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
        customerId: "cus_123456789",
        metadata: {
          rentalData: JSON.stringify(complexRentalData),
        },
      });
    });
  });

  describe("Error handling and edge cases", () => {
    it("should handle malformed JSON in rental data", async () => {
      const mockUser = {
        id: 1,
        email: "test@example.com",
        firstName: "John",
        lastName: "Doe",
        stripeCustomerId: "cus_123456789",
      };

      User.findByPk.mockResolvedValue(mockUser);

      // This should work fine as Express will parse valid JSON
      const response = await request(app)
        .post("/stripe/create-setup-checkout-session")
        .set("Authorization", "Bearer valid_token")
        .set("Content-Type", "application/json")
        .send('{"rentalData":{"itemId":"123"}}');

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

    it("should handle very large session IDs", async () => {
      const longSessionId = "cs_" + "a".repeat(100);
      const error = new Error("Session ID too long");

      StripeService.getCheckoutSession.mockRejectedValue(error);

      const response = await request(app).get(
        `/stripe/checkout-session/${longSessionId}`
      );

      expect(response.status).toBe(500);
      expect(response.body).toEqual({ error: "Session ID too long" });
    });

    it("should handle concurrent requests for same user", async () => {
      const mockUser = {
        id: 1,
        email: "test@example.com",
        stripeConnectedAccountId: null,
        update: jest.fn().mockResolvedValue({}),
      };

      const mockAccount = { id: "acct_123456789" };

      User.findByPk.mockResolvedValue(mockUser);
      StripeService.createConnectedAccount.mockResolvedValue(mockAccount);

      // Simulate concurrent requests
      const [response1, response2] = await Promise.all([
        request(app)
          .post("/stripe/accounts")
          .set("Authorization", "Bearer valid_token"),
        request(app)
          .post("/stripe/accounts")
          .set("Authorization", "Bearer valid_token"),
      ]);

      // Both should succeed (in this test scenario)
      expect(response1.status).toBe(200);
      expect(response2.status).toBe(200);
    });
  });
});
