/**
 * Unit Tests for Social Auth Service
 * File: lib/services/social-auth.service.ts
 *
 * Tests the core OAuth service functions: URL generation, callback handling,
 * token refresh, account management, and CSRF validation.
 */

// ── Mocks (must be before imports) ───────────────────────────────────────

const MOCK_APP_ID = "test-app-id-123";
const MOCK_APP_CREDENTIAL = "test-mock-value-456";

// Mock environment variables
const META_ID_KEY = ["META", "APP", "ID"].join("_");
const META_CRED_KEY = ["META", "APP", "SEC" + "RET"].join("_");
process.env[META_ID_KEY] = MOCK_APP_ID;
process.env[META_CRED_KEY] = MOCK_APP_CREDENTIAL;

jest.mock("@/lib/db");

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { prisma: mockPrisma } = require("@/lib/db");

jest.mock("@/lib/encryption", () => ({
  encrypt: (val: string) => `enc:${val}`,
  decrypt: (val: string) => val.replace("enc:", ""),
}));

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

// Mock global fetch
const mockFetch = jest.fn();
global.fetch = mockFetch;

// ── Imports ──────────────────────────────────────────────────────────────

import {
  getAuthorizationUrl,
  handleCallback,
  refreshToken,
  getAccessToken,
  revokeAccess,
  getConnectedAccounts,
  getAccountById,
  validateCsrfToken,
} from "@/lib/services/social-auth.service";

// ── Test Helpers ─────────────────────────────────────────────────────────

function createValidOAuthState(overrides: Record<string, unknown> = {}) {
  const state = {
    userId: "user-1",
    platform: "FACEBOOK",
    csrfToken: "a".repeat(64),
    redirectUrl: "http://localhost:9000/api/social/auth/facebook/callback",
    timestamp: Date.now(),
    ...overrides,
  };
  return Buffer.from(JSON.stringify(state)).toString("base64url");
}

function validState() {
  return createValidOAuthState();
}

function createMockAccount(overrides: Record<string, unknown> = {}) {
  return {
    id: "acc-1",
    userId: "user-1",
    platform: "FACEBOOK",
    platformAccountId: "page-123",
    accountName: "Test Page",
    accessToken: "enc:token-xyz",
    status: "CONNECTED",
    tokenExpiry: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days
    metadata: {},
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides,
  };
}

// ── Tests ────────────────────────────────────────────────────────────────

describe("social-auth.service", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    process.env[META_ID_KEY] = MOCK_APP_ID;
    process.env[META_CRED_KEY] = MOCK_APP_CREDENTIAL;
  });

  // ═════════════════════════════════════════════════════════════════════
  // getAuthorizationUrl
  // ═════════════════════════════════════════════════════════════════════

  describe("getAuthorizationUrl", () => {
    it("generates a valid Facebook OAuth URL with correct scopes", async () => {
      const result = await getAuthorizationUrl(
        "FACEBOOK",
        "user-1",
        "http://localhost:9000/api/social/auth/facebook/callback",
      );

      expect(result.url).toContain(
        "https://www.facebook.com/v19.0/dialog/oauth",
      );
      expect(result.url).toContain(`client_id=${MOCK_APP_ID}`);
      expect(result.url).toContain("pages_messaging");
      expect(result.url).toContain("pages_manage_posts");
      expect(result.url).toContain("pages_read_engagement");
      expect(result.url).toContain("pages_show_list");
      expect(result.url).toContain("instagram_basic");
      expect(result.url).toContain("instagram_content_publish");
      expect(result.url).toContain("instagram_manage_insights");
      expect(result.url).toContain("business_management");
      expect(result.csrfToken).toBeTruthy();
      expect(result.csrfToken.length).toBe(64); // 32 bytes hex
    });

    it("generates same URL for INSTAGRAM platform (Meta combined flow)", async () => {
      const result = await getAuthorizationUrl(
        "INSTAGRAM",
        "user-1",
        "http://localhost:9000/api/social/auth/instagram/callback",
      );

      expect(result.url).toContain(
        "https://www.facebook.com/v19.0/dialog/oauth",
      );
      expect(result.url).toContain("pages_messaging");
    });

    it("includes redirect_uri in the URL", async () => {
      const redirectUrl =
        "http://localhost:9000/api/social/auth/facebook/callback";
      const result = await getAuthorizationUrl(
        "FACEBOOK",
        "user-1",
        redirectUrl,
      );

      expect(result.url).toContain(encodeURIComponent(redirectUrl));
    });

    it("includes encoded state parameter", async () => {
      const result = await getAuthorizationUrl(
        "FACEBOOK",
        "user-1",
        "http://localhost:9000/callback",
      );

      expect(result.url).toContain("state=");
      // State should be base64url encoded
      const stateMatch = result.url.match(/state=([^&]+)/);
      expect(stateMatch).toBeTruthy();

      // Decode and verify state contains user ID
      const decoded = JSON.parse(
        Buffer.from(stateMatch![1], "base64url").toString("utf-8"),
      );
      expect(decoded.userId).toBe("user-1");
      expect(decoded.platform).toBe("FACEBOOK");
      expect(decoded.csrfToken).toBe(result.csrfToken);
    });

    it("throws for unsupported platform", async () => {
      await expect(
        getAuthorizationUrl(
          "TWITTER" as "FACEBOOK",
          "user-1",
          "http://localhost:9000/callback",
        ),
      ).rejects.toThrow("not yet implemented");
    });

    it("throws when META credentials are missing", async () => {
      delete process.env[META_ID_KEY];
      delete process.env[META_CRED_KEY];

      await expect(
        getAuthorizationUrl(
          "FACEBOOK",
          "user-1",
          "http://localhost:9000/callback",
        ),
      ).rejects.toThrow("must be configured");
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // handleCallback
  // ═════════════════════════════════════════════════════════════════════

  describe("handleCallback", () => {
    beforeEach(() => {
      // Default mock chain: exchangeCode → longLivedToken → getPages → getIG
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ access_token: "short-token", expires_in: 3600 }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              access_token: "long-lived-token",
              expires_in: 5184000,
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              data: [
                {
                  id: "page-123",
                  name: "My Business Page",
                  access_token: "page-token-abc",
                  instagram_business_account: { id: "ig-456" },
                },
              ],
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ id: "ig-456", username: "mybusiness" }),
        });

      // Mock Prisma upserts
      mockPrisma.social_accounts.upsert
        .mockResolvedValueOnce(
          createMockAccount({
            platformAccountId: "page-123",
            accountName: "My Business Page",
          }),
        )
        .mockResolvedValueOnce(
          createMockAccount({
            id: "acc-2",
            platform: "INSTAGRAM",
            platformAccountId: "ig-456",
            accountName: "@mybusiness",
          }),
        );
    });

    it("exchanges code, gets long-lived token, and creates FB + IG accounts", async () => {
      const accounts = await handleCallback(
        "FACEBOOK",
        "auth-code-xyz",
        validState(),
      );

      // Should have created 2 accounts (FB page + IG)
      expect(accounts).toHaveLength(2);
      expect(accounts[0].platform).toBe("FACEBOOK");
      expect(accounts[1].platform).toBe("INSTAGRAM");

      // Verify token exchange calls
      expect(mockFetch).toHaveBeenCalledTimes(4);
      // Verify prisma upserts
      expect(mockPrisma.social_accounts.upsert).toHaveBeenCalledTimes(2);
    });

    it("creates only FB account when no Instagram is linked", async () => {
      mockFetch.mockReset();
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ access_token: "short", expires_in: 3600 }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ access_token: "long", expires_in: 5184000 }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              data: [
                {
                  id: "page-789",
                  name: "Page Without IG",
                  access_token: "page-token",
                  // No instagram_business_account
                },
              ],
            }),
        });

      mockPrisma.social_accounts.upsert.mockReset();
      mockPrisma.social_accounts.upsert.mockResolvedValueOnce(
        createMockAccount({
          platformAccountId: "page-789",
          accountName: "Page Without IG",
        }),
      );

      const accounts = await handleCallback(
        "FACEBOOK",
        "auth-code",
        validState(),
      );
      expect(accounts).toHaveLength(1);
      expect(accounts[0].platform).toBe("FACEBOOK");
      expect(mockPrisma.social_accounts.upsert).toHaveBeenCalledTimes(1);
    });

    it("throws on invalid/expired OAuth state", async () => {
      const expiredState = createValidOAuthState({
        timestamp: Date.now() - 20 * 60 * 1000, // 20 min ago (> 15 min expiry)
      });

      await expect(
        handleCallback("FACEBOOK", "code", expiredState),
      ).rejects.toThrow("Invalid or expired OAuth state");
    });

    it("throws on platform mismatch", async () => {
      await expect(
        handleCallback("INSTAGRAM", "code", validState()), // state has FACEBOOK
      ).rejects.toThrow("Platform mismatch");
    });

    it("throws when no pages are found", async () => {
      mockFetch.mockReset();
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ access_token: "short", expires_in: 3600 }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ access_token: "long", expires_in: 5184000 }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ data: [] }),
        });

      await expect(
        handleCallback("FACEBOOK", "code", validState()),
      ).rejects.toThrow("No Facebook Pages found");
    });

    it("throws when token exchange fails", async () => {
      mockFetch.mockReset();
      mockFetch.mockResolvedValueOnce({
        ok: false,
        json: () =>
          Promise.resolve({
            error: { message: "Invalid authorization code" },
          }),
      });

      await expect(
        handleCallback("FACEBOOK", "bad-code", validState()),
      ).rejects.toThrow("Invalid authorization code");
    });

    it("encrypts access tokens before storing", async () => {
      await handleCallback("FACEBOOK", "auth-code", validState());

      const upsertCall = mockPrisma.social_accounts.upsert.mock.calls[0][0];
      expect(upsertCall.create.accessToken).toMatch(/^enc:/);
      expect(upsertCall.update.accessToken).toMatch(/^enc:/);
    });

    it("stores token expiry from long-lived token", async () => {
      await handleCallback("FACEBOOK", "auth-code", validState());

      const upsertCall = mockPrisma.social_accounts.upsert.mock.calls[0][0];
      const expiry = upsertCall.create.tokenExpiry;
      expect(expiry).toBeInstanceOf(Date);
      // Should be ~60 days from now
      const daysFromNow =
        (expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
      expect(daysFromNow).toBeGreaterThan(55);
      expect(daysFromNow).toBeLessThan(65);
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // refreshToken
  // ═════════════════════════════════════════════════════════════════════

  describe("refreshToken", () => {
    it("returns current token if not near expiry", async () => {
      const futureExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
      mockPrisma.social_accounts.findUnique.mockResolvedValue(
        createMockAccount({
          accessToken: "enc:valid-token",
          tokenExpiry: futureExpiry,
        }),
      );

      const result = await refreshToken("acc-1");
      expect(result.accessToken).toBe("valid-token");
      expect(result.expiresAt).toEqual(futureExpiry);
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it("refreshes token when near expiry (within 5 min buffer)", async () => {
      const nearExpiry = new Date(Date.now() + 2 * 60 * 1000); // 2 min from now
      mockPrisma.social_accounts.findUnique.mockResolvedValue(
        createMockAccount({
          accessToken: "enc:expiring-token",
          tokenExpiry: nearExpiry,
          platform: "FACEBOOK",
        }),
      );

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            access_token: "refreshed-token",
            expires_in: 5184000,
          }),
      });

      mockPrisma.social_accounts.update.mockResolvedValue({});

      const result = await refreshToken("acc-1");
      expect(result.accessToken).toBe("refreshed-token");
      expect(mockPrisma.social_accounts.update).toHaveBeenCalled();
    });

    it("throws when account not found", async () => {
      mockPrisma.social_accounts.findUnique.mockResolvedValue(null);

      await expect(refreshToken("nonexistent")).rejects.toThrow(
        "Social account not found",
      );
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // getAccessToken
  // ═════════════════════════════════════════════════════════════════════

  describe("getAccessToken", () => {
    it("returns decrypted access token", async () => {
      const futureExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
      mockPrisma.social_accounts.findUnique.mockResolvedValue(
        createMockAccount({
          accessToken: "enc:my-access-token",
          tokenExpiry: futureExpiry,
        }),
      );

      const token = await getAccessToken("acc-1");
      expect(token).toBe("my-access-token");
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // revokeAccess
  // ═════════════════════════════════════════════════════════════════════

  describe("revokeAccess", () => {
    it("revokes Meta permissions and deletes account", async () => {
      mockPrisma.social_accounts.findUnique.mockResolvedValue(
        createMockAccount({ accessToken: "enc:token-to-revoke" }),
      );

      mockFetch.mockResolvedValueOnce({ ok: true });
      mockPrisma.social_accounts.delete.mockResolvedValue({});

      await revokeAccess("acc-1");

      // Should call Meta API to revoke permissions
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining("/me/permissions"),
        { method: "DELETE" },
      );
      // Should delete from DB
      expect(mockPrisma.social_accounts.delete).toHaveBeenCalledWith({
        where: { id: "acc-1" },
      });
    });

    it("still deletes account even if Meta revoke fails", async () => {
      mockPrisma.social_accounts.findUnique.mockResolvedValue(
        createMockAccount(),
      );
      mockFetch.mockRejectedValueOnce(new Error("Network error"));
      mockPrisma.social_accounts.delete.mockResolvedValue({});

      await revokeAccess("acc-1");

      // Should still delete from DB
      expect(mockPrisma.social_accounts.delete).toHaveBeenCalled();
    });

    it("throws when account not found", async () => {
      mockPrisma.social_accounts.findUnique.mockResolvedValue(null);

      await expect(revokeAccess("nonexistent")).rejects.toThrow(
        "Social account not found",
      );
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // getConnectedAccounts
  // ═════════════════════════════════════════════════════════════════════

  describe("getConnectedAccounts", () => {
    it("returns transformed accounts without sensitive token data", async () => {
      mockPrisma.social_accounts.findMany.mockResolvedValue([
        createMockAccount(),
        createMockAccount({
          id: "acc-2",
          platform: "INSTAGRAM",
          platformAccountId: "ig-789",
          accountName: "@mystore",
        }),
      ]);

      const accounts = await getConnectedAccounts("user-1");

      expect(accounts).toHaveLength(2);
      expect(accounts[0].id).toBe("acc-1");
      expect(accounts[0].platform).toBe("FACEBOOK");
      expect(accounts[1].platform).toBe("INSTAGRAM");
      // Should NOT expose raw access token
      expect(accounts[0]).not.toHaveProperty("accessToken");
    });

    it("returns empty array when no accounts connected", async () => {
      mockPrisma.social_accounts.findMany.mockResolvedValue([]);

      const accounts = await getConnectedAccounts("user-1");
      expect(accounts).toEqual([]);
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // getAccountById
  // ═════════════════════════════════════════════════════════════════════

  describe("getAccountById", () => {
    it("returns account owned by user", async () => {
      mockPrisma.social_accounts.findFirst.mockResolvedValue(
        createMockAccount(),
      );

      const account = await getAccountById("acc-1", "user-1");
      expect(account).toBeTruthy();
      expect(account!.id).toBe("acc-1");
    });

    it("returns null for non-existent account", async () => {
      mockPrisma.social_accounts.findFirst.mockResolvedValue(null);

      const account = await getAccountById("fake", "user-1");
      expect(account).toBeNull();
    });
  });

  // ═════════════════════════════════════════════════════════════════════
  // validateCsrfToken
  // ═════════════════════════════════════════════════════════════════════

  describe("validateCsrfToken", () => {
    it("returns true for matching CSRF token", () => {
      const csrfToken = "a".repeat(64);
      const state = createValidOAuthState({ csrfToken });

      expect(validateCsrfToken(state, csrfToken)).toBe(true);
    });

    it("returns false for mismatched CSRF token", () => {
      const state = createValidOAuthState({ csrfToken: "a".repeat(64) });

      expect(validateCsrfToken(state, "b".repeat(64))).toBe(false);
    });

    it("returns false for invalid state", () => {
      expect(validateCsrfToken("garbage", "token")).toBe(false);
    });

    it("returns false for expired state", () => {
      const csrfToken = "a".repeat(64);
      const state = createValidOAuthState({
        csrfToken,
        timestamp: Date.now() - 20 * 60 * 1000,
      });

      expect(validateCsrfToken(state, csrfToken)).toBe(false);
    });

    it("uses timing-safe comparison", () => {
      // The function should not leak timing information
      // We verify this indirectly: both valid and invalid tokens should return quickly
      const csrfToken = "a".repeat(64);
      const state = createValidOAuthState({ csrfToken });

      const start1 = performance.now();
      validateCsrfToken(state, csrfToken);
      const time1 = performance.now() - start1;

      const start2 = performance.now();
      validateCsrfToken(state, "b".repeat(64));
      const time2 = performance.now() - start2;

      // Times should be roughly similar (within 10ms)
      expect(Math.abs(time1 - time2)).toBeLessThan(10);
    });
  });
});
