/**
 * Integration Tests for Account Routes
 *
 * Tests account profile, credits, usage, subscription, and API key management
 */

// Mock queue BEFORE any imports to prevent Redis connection
jest.mock('../../../queue', () => ({
  translationQueue: {
    add: jest.fn<() => Promise<{ id: string }>>().mockResolvedValue({ id: 'mock-job-id' }),
    process: jest.fn(),
    on: jest.fn(),
    close: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
    getJob: jest.fn<() => Promise<null>>().mockResolvedValue(null),
    getJobs: jest.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
    pause: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
    resume: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
    clean: jest.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
    empty: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
  },
}));

import { beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
import request from 'supertest';
import { createServer } from '../../../server';
import { prismaMock } from '../../setup';
import { generateAccessToken } from '../../../auth/jwtService';
import { tierService } from '../../../services/tierService';
import { Application } from 'express';

describe('Account Routes', () => {
  let app: Application;

  beforeAll(() => {
    app = createServer();
  });

  const mockUser = {
    id: 'user_123',
    email: 'test@example.com',
    password_hash: '$2b$12$mockhashedpassword',
    email_verified: true,
    status: 'active',
    created_at: new Date('2024-01-01'),
    updated_at: new Date('2024-01-01'),
  };

  const mockSubscription = {
    id: 'sub_123',
    user_id: 'user_123',
    plugin: 'translate',
    plan_tier: 'professional',
    billing_cycle: 'monthly',
    status: 'active',
    current_period_start: new Date('2024-01-01'),
    current_period_end: new Date('2024-02-01'),
    cancel_at_period_end: false,
    paypal_subscription_id: 'PAYPAL_SUB_123',
    created_at: new Date('2024-01-01'),
    updated_at: new Date('2024-01-01'),
  };

  /** Row shape read by tierService.refreshCache() from the subscription_tiers table. */
  const tierRow = (slug: string, creditAllocation: number) => ({
    id: `tier_${slug}`,
    slug,
    plugin: 'translate',
    name: slug,
    description: `${slug} tier`,
    monthly_price: '10.00',
    annual_price: '100.00',
    credit_allocation: creditAllocation,
    allowed_models: ['gemini-3-flash-preview'],
    features: [],
    rate_limit: 60,
    paypal_plan_monthly: `P-${slug}-M`,
    paypal_plan_annual: `P-${slug}-A`,
    is_active: true,
    display_order: 1,
  });

  const mockTiers = [
    tierRow('starter', 100000),
    tierRow('professional', 500000),
    tierRow('enterprise', 2000000),
  ];

  /**
   * API key row as authenticateApiKey reads it: nested user with its subscriptions,
   * matched by key_hash on apiKey.findUnique.
   */
  const mockApiKeyRecord = {
    id: 'key_auth_1',
    user_id: 'user_123',
    key_hash: 'hashed_lookup_value',
    prefix: 'sk_live_test_key',
    name: 'Auth Key',
    is_active: true,
    last_used_at: null,
    created_at: new Date('2024-01-01'),
    user: {
      ...mockUser,
      subscriptions: [mockSubscription],
    },
  };

  const API_KEY = 'sk_live_test_key_123';

  beforeEach(() => {
    // tierService keeps a module-level cache; an unstubbed findMany poisons it for
    // every later test in the file.
    prismaMock.subscriptionTier.findMany.mockResolvedValue(mockTiers as any);
    tierService.clearCache();

    prismaMock.apiKey.findUnique.mockResolvedValue(mockApiKeyRecord as any);
    prismaMock.apiKey.update.mockResolvedValue(mockApiKeyRecord as any);
  });

  describe('GET /v1/account (Profile)', () => {
    it('should get authenticated user profile with subscription', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 50000,
      } as any);

      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.user).toMatchObject({
        id: 'user_123',
        email: 'test@example.com',
        emailVerified: true,
        status: 'active',
      });

      expect(response.body.subscriptions).toHaveLength(1);
      expect(response.body.subscriptions[0]).toMatchObject({
        plugin: 'translate',
        planTier: 'professional',
        billingCycle: 'monthly',
        status: 'active',
        cancelAtPeriodEnd: false,
      });

      expect(response.body.credits).toMatchObject({
        balance: 50000,
        allocation: 500000, // Professional tier allocation
      });

      // Ensure password_hash is NOT returned
      expect(response.body.user.password_hash).toBeUndefined();
      expect(response.body.user.passwordHash).toBeUndefined();
    });

    it('should return 401 for missing token', async () => {
      const response = await request(app)
        .get('/v1/account')
        .expect(401);

      // authenticateJWT uses a flat error envelope (not errorHandler.errorResponse)
      expect(response.body.error).toBe(true);
      expect(response.body.code).toBe('MISSING_TOKEN');
    });

    it('should return 401 for invalid token', async () => {
      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', 'Bearer invalid_token')
        .expect(401);

      expect(response.body.error).toBe(true);
      expect(response.body.code).toBe('INVALID_TOKEN');
    });
  });

  describe('GET /v1/account/credits', () => {
    it('should get current credit balance with subscription', async () => {
      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 25000,
      } as any);

      const response = await request(app)
        .get('/v1/account/credits')
        .set('Authorization', `Bearer ${API_KEY}`)
        .expect(200);

      expect(response.body.credits_balance).toBe(25000);
      expect(response.body.subscription).toMatchObject({
        tier: 'professional',
        status: 'active',
      });
    });

    it('should handle users with no credit transactions', async () => {
      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue(null);

      const response = await request(app)
        .get('/v1/account/credits')
        .set('Authorization', `Bearer ${API_KEY}`)
        .expect(200);

      expect(response.body.credits_balance).toBe(0);
    });

    it('should handle users without subscription', async () => {
      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 10000,
      } as any);

      const response = await request(app)
        .get('/v1/account/credits')
        .set('Authorization', `Bearer ${API_KEY}`)
        .expect(200);

      expect(response.body.credits_balance).toBe(10000);
      expect(response.body.subscription).toBeNull();
    });

    it('should return 401 for unauthenticated requests', async () => {
      const response = await request(app)
        .get('/v1/account/credits')
        .expect(401);

      // authenticateApiKey uses errorHandler.errorResponse (nested envelope)
      expect(response.body.success).toBe(false);
      expect(response.body.error.code).toBe('MISSING_API_KEY');
    });

    it('should return 401 for an unknown API key', async () => {
      prismaMock.apiKey.findUnique.mockResolvedValue(null);

      const response = await request(app)
        .get('/v1/account/credits')
        .set('Authorization', `Bearer ${API_KEY}`)
        .expect(401);

      expect(response.body.error.code).toBe('INVALID_API_KEY');
    });
  });

  describe('GET /v1/account/usage', () => {
    it('should get usage statistics with pagination', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);

      const mockTransactions = [
        {
          id: 'tx_1',
          user_id: 'user_123',
          type: 'deduction',
          amount: -500,
          balance_after: 49500,
          description: 'Translation job',
          created_at: new Date('2024-01-15'),
        },
        {
          id: 'tx_2',
          user_id: 'user_123',
          type: 'allocation',
          amount: 50000,
          balance_after: 50000,
          description: 'Monthly allocation',
          created_at: new Date('2024-01-01'),
        },
      ];

      const mockJobs = [
        {
          id: 'job_1',
          user_id: 'user_123',
          status: 'completed',
          source_lang: 'en',
          target_lang: 'es',
          model: 'gemini-3-flash-preview',
          tokens_used: 500,
          cost: 0.005,
          created_at: new Date('2024-01-15'),
        },
      ];

      prismaMock.creditTransaction.findMany.mockResolvedValue(mockTransactions as any);
      prismaMock.translationJob.findMany.mockResolvedValue(mockJobs as any);
      prismaMock.creditTransaction.count.mockResolvedValue(2);

      const response = await request(app)
        .get('/v1/account/usage?page=1&limit=50')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.transactions).toHaveLength(2);
      expect(response.body.jobs).toHaveLength(1);
      expect(response.body.pagination).toMatchObject({
        page: 1,
        limit: 50,
        total: 2,
        totalPages: 1,
      });
    });

    it('should apply pagination correctly', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);
      prismaMock.creditTransaction.findMany.mockResolvedValue([]);
      prismaMock.translationJob.findMany.mockResolvedValue([]);
      prismaMock.creditTransaction.count.mockResolvedValue(0);

      const response = await request(app)
        .get('/v1/account/usage?page=2&limit=10')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.pagination).toMatchObject({
        page: 2,
        limit: 10,
        total: 0,
        totalPages: 0,
      });

      expect(prismaMock.creditTransaction.findMany).toHaveBeenCalledWith(
        expect.objectContaining({ take: 10, skip: 10, where: { user_id: 'user_123' } })
      );
    });

    it('should return empty usage for new accounts', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);
      prismaMock.creditTransaction.findMany.mockResolvedValue([]);
      prismaMock.translationJob.findMany.mockResolvedValue([]);
      prismaMock.creditTransaction.count.mockResolvedValue(0);

      const response = await request(app)
        .get('/v1/account/usage')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.transactions).toHaveLength(0);
      expect(response.body.jobs).toHaveLength(0);
      expect(response.body.pagination.total).toBe(0);
    });
  });

  describe('GET /v1/account subscription payload', () => {
    it('should get active subscription details', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [mockSubscription],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 50000,
      } as any);

      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.subscriptions[0]).toMatchObject({
        plugin: 'translate',
        planTier: 'professional',
        billingCycle: 'monthly',
        status: 'active',
        cancelAtPeriodEnd: false,
      });
      expect(response.body.subscriptions[0].currentPeriodEnd).toBe(
        mockSubscription.current_period_end.toISOString()
      );
    });

    it('should return an empty subscription list for users without subscription', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'starter', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 10000,
      } as any);

      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.subscriptions).toEqual([]);
      expect(response.body.credits.allocation).toBe(0);
    });

    it('should include plan tier and billing cycle', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'enterprise', 'active');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [
          {
            ...mockSubscription,
            plan_tier: 'enterprise',
            billing_cycle: 'annual',
          },
        ],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 200000,
      } as any);

      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.subscriptions[0].planTier).toBe('enterprise');
      expect(response.body.subscriptions[0].billingCycle).toBe('annual');
      expect(response.body.credits.allocation).toBe(2000000); // Enterprise tier allocation
    });

    it('should show next billing date', async () => {
      const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

      const futureDate = new Date('2024-02-01');

      prismaMock.user.findUnique.mockResolvedValue({
        ...mockUser,
        subscriptions: [
          {
            ...mockSubscription,
            current_period_end: futureDate,
          },
        ],
      } as any);

      prismaMock.creditTransaction.findFirst.mockResolvedValue({
        balance_after: 50000,
      } as any);

      const response = await request(app)
        .get('/v1/account')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);

      expect(response.body.subscriptions[0].currentPeriodEnd).toBe(futureDate.toISOString());
    });
  });

  describe('API Keys Management', () => {
    describe('POST /v1/account/api-keys', () => {
      it('should create new API key with name', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);

        const mockApiKey = {
          id: 'key_123',
          user_id: 'user_123',
          key_hash: 'hashed_key',
          prefix: 'sk_live_abc',
          name: 'Production API Key',
          is_active: true,
          last_used_at: null,
          created_at: new Date('2024-01-01'),
        };

        prismaMock.apiKey.create.mockResolvedValue(mockApiKey as any);

        const response = await request(app)
          .post('/v1/account/api-keys')
          .set('Authorization', `Bearer ${token}`)
          .send({ name: 'Production API Key' })
          .expect(201);

        expect(response.body.key).toBeDefined();
        expect(response.body.key).toMatch(/^sk_live_[0-9a-f]+$/);
        expect(response.body.name).toBe('Production API Key');
        expect(response.body.prefix).toBe('sk_live_abc');
        expect(response.body.isActive).toBe(true);
        // The stored value is the hash, never the plaintext key
        expect(prismaMock.apiKey.create).toHaveBeenCalledWith(
          expect.objectContaining({
            data: expect.objectContaining({ user_id: 'user_123', name: 'Production API Key' }),
          })
        );
        const createArgs = prismaMock.apiKey.create.mock.calls[0][0] as any;
        expect(createArgs.data.key_hash).not.toBe(response.body.key);
      });

      it('should validate key name requirements', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);

        const response = await request(app)
          .post('/v1/account/api-keys')
          .set('Authorization', `Bearer ${token}`)
          .send({ name: '' })
          .expect(400);

        // validator.validate() uses the flat Zod error envelope
        expect(response.body.error).toBe(true);
        expect(response.body.code).toBe('VALIDATION_ERROR');
        expect(prismaMock.apiKey.create).not.toHaveBeenCalled();
      });
    });

    describe('GET /v1/account/api-keys', () => {
      it('should list all user API keys without secrets', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);

        const mockApiKeys = [
          {
            id: 'key_1',
            user_id: 'user_123',
            key_hash: 'hash1',
            prefix: 'sk_live_abc',
            name: 'Production Key',
            is_active: true,
            last_used_at: new Date('2024-01-10'),
            created_at: new Date('2024-01-01'),
          },
          {
            id: 'key_2',
            user_id: 'user_123',
            key_hash: 'hash2',
            prefix: 'sk_live_xyz',
            name: 'Staging Key',
            is_active: false,
            last_used_at: null,
            created_at: new Date('2024-01-05'),
          },
        ];

        prismaMock.apiKey.findMany.mockResolvedValue(mockApiKeys as any);

        const response = await request(app)
          .get('/v1/account/api-keys')
          .set('Authorization', `Bearer ${token}`)
          .expect(200);

        expect(response.body.apiKeys).toHaveLength(2);
        expect(response.body.apiKeys[0]).toMatchObject({
          id: 'key_1',
          name: 'Production Key',
          prefix: 'sk_live_abc',
          isActive: true,
        });
        // Ensure full key is NOT returned
        expect(response.body.apiKeys[0].key).toBeUndefined();
        expect(response.body.apiKeys[0].key_hash).toBeUndefined();
        expect(response.body.apiKeys[0].keyHash).toBeUndefined();
        // Listing is scoped to the authenticated user
        expect(prismaMock.apiKey.findMany).toHaveBeenCalledWith(
          expect.objectContaining({ where: { user_id: 'user_123' } })
        );
      });
    });

    describe('DELETE /v1/account/api-keys/:keyId', () => {
      it('should revoke API key', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);

        prismaMock.apiKey.updateMany.mockResolvedValue({ count: 1 } as any);

        const response = await request(app)
          .delete('/v1/account/api-keys/key_123')
          .set('Authorization', `Bearer ${token}`)
          .expect(200);

        expect(response.body.message).toContain('revoked successfully');
        expect(prismaMock.apiKey.updateMany).toHaveBeenCalledWith({
          where: { id: 'key_123', user_id: 'user_123' },
          data: { is_active: false },
        });
      });

      it('should return 404 for non-existent keys without mutating anything', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);
        prismaMock.apiKey.updateMany.mockResolvedValue({ count: 0 } as any);
        prismaMock.apiKey.update.mockClear();

        const response = await request(app)
          .delete('/v1/account/api-keys/nonexistent_key')
          .set('Authorization', `Bearer ${token}`)
          .expect(404);

        expect(response.body.error.code).toBe('NOT_FOUND');
        expect(prismaMock.apiKey.updateMany).toHaveBeenCalledWith({
          where: { id: 'nonexistent_key', user_id: 'user_123' },
          data: { is_active: false },
        });
        expect(prismaMock.apiKey.update).not.toHaveBeenCalled();
      });

      it('should ensure user can only access their own keys', async () => {
        const token = generateAccessToken('user_123', 'test@example.com', 'professional', 'active');

        prismaMock.user.findUnique.mockResolvedValue({
          ...mockUser,
          subscriptions: [mockSubscription],
        } as any);

        prismaMock.apiKey.updateMany.mockResolvedValue({ count: 0 } as any);
        prismaMock.apiKey.update.mockClear();

        const response = await request(app)
          .delete('/v1/account/api-keys/other_user_key')
          .set('Authorization', `Bearer ${token}`)
          .expect(404);

        expect(response.body.error.code).toBe('NOT_FOUND');
        expect(prismaMock.apiKey.updateMany).toHaveBeenCalledWith({
          where: { id: 'other_user_key', user_id: 'user_123' },
          data: { is_active: false },
        });
        expect(prismaMock.apiKey.update).not.toHaveBeenCalled();
      });
    });
  });
});
