import { describe, it, expect, vi, afterEach } from 'vitest';
import { createSubscriptionClient } from './client.js';
import type { SubscriptionDoc, PlanConfig } from './types.js';

const baseConfig = {
  baseUrl: 'http://localhost:4003/api',
  productId: 'testapp',
  userId: 'user-1',
  getAccessToken: () => 'test-token',
};

function mockSub(overrides?: Partial<SubscriptionDoc>): SubscriptionDoc {
  return {
    id: 'sub-1',
    productId: 'testapp',
    userId: 'user-1',
    plan: 'pro',
    status: 'active',
    currentPeriodStart: '2026-01-01T00:00:00Z',
    currentPeriodEnd: '2026-12-31T23:59:59Z',
    cancelAtPeriodEnd: false,
    monthlyPrice: 999,
    tokensIncluded: 10000,
    tokensUsed: 500,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    ...overrides,
  };
}

function mockPlan(overrides?: Partial<PlanConfig>): PlanConfig {
  return {
    id: 'plan-1',
    productId: 'testapp',
    name: 'pro',
    displayName: 'Pro',
    price: 999,
    tokens: 10000,
    words: 0,
    dictations: 0,
    features: ['ai_coaching', 'advanced_stats'],
    active: true,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    ...overrides,
  };
}

describe('createSubscriptionClient', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should get subscription by userId', async () => {
    const sub = mockSub();
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(sub),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    const result = await client.getMySubscription();

    expect(result).toEqual(sub);
    const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
    expect(fetchMock).toHaveBeenCalledWith(
      'http://localhost:4003/api/subscriptions/user-1',
      expect.any(Object)
    );
  });

  it('should return null for 404 on getMySubscription', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 404,
      })
    );

    const client = createSubscriptionClient(baseConfig);
    const result = await client.getMySubscription();
    expect(result).toBeNull();
  });

  it('should unwrap .plans from getPlans response', async () => {
    const plans = [mockPlan(), mockPlan({ name: 'free', displayName: 'Free', features: [] })];
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ plans }),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    const result = await client.getPlans();
    expect(result).toHaveLength(2);
    expect(result[0].name).toBe('pro');
  });

  it('should start a trial', async () => {
    const sub = mockSub({ status: 'trialing' });
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(sub),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    const result = await client.startTrial();
    expect(result.status).toBe('trialing');
  });

  it('should cancel subscription', async () => {
    const sub = mockSub({ cancelAtPeriodEnd: true });
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(sub),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    const result = await client.cancelSubscription();
    expect(result.cancelAtPeriodEnd).toBe(true);
  });

  it('should report isPro correctly from cache', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockSub({ plan: 'pro', status: 'active' })),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    expect(client.isPro()).toBe(false); // no cache yet

    await client.getMySubscription();
    expect(client.isPro()).toBe(true);
  });

  it('should report isPro false for free plan', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockSub({ plan: 'free', status: 'active' })),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await client.getMySubscription();
    expect(client.isPro()).toBe(false);
  });

  it('should check hasFeature from cached plans', async () => {
    let callIndex = 0;
    vi.stubGlobal(
      'fetch',
      vi.fn().mockImplementation(() => {
        callIndex++;
        if (callIndex === 1) {
          return Promise.resolve({
            ok: true,
            json: () => Promise.resolve(mockSub({ plan: 'pro' })),
          });
        }
        return Promise.resolve({
          ok: true,
          json: () =>
            Promise.resolve({
              plans: [mockPlan({ name: 'pro', features: ['ai_coaching', 'advanced_stats'] })],
            }),
        });
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await client.refresh();

    expect(client.hasFeature('ai_coaching')).toBe(true);
    expect(client.hasFeature('nonexistent')).toBe(false);
  });

  it('should report isTrialing correctly', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockSub({ status: 'trialing' })),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await client.getMySubscription();
    expect(client.isTrialing()).toBe(true);
  });

  it('should calculate daysRemaining', async () => {
    const futureDate = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000).toISOString();
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockSub({ currentPeriodEnd: futureDate })),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await client.getMySubscription();
    const days = client.daysRemaining();
    expect(days).toBeGreaterThanOrEqual(10);
    expect(days).toBeLessThanOrEqual(11);
  });

  it('should persist and restore from storage', async () => {
    const store: Record<string, string> = {};
    const storage = {
      getItem: (k: string) => store[k] ?? null,
      setItem: (k: string, v: string) => {
        store[k] = v;
      },
    };

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockSub()),
      })
    );

    const client1 = createSubscriptionClient({ ...baseConfig, storage });
    await client1.getMySubscription();

    expect(store['testapp-subscription']).toBeDefined();

    // Create new client — should restore from storage
    const client2 = createSubscriptionClient({ ...baseConfig, storage });
    expect(client2.getCachedSubscription()).not.toBeNull();
    expect(client2.isPro()).toBe(true);
  });

  it('should send correct headers', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ plans: [] }),
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await client.getPlans();

    const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
    const callHeaders = fetchMock.mock.calls[0][1].headers as Record<string, string>;
    expect(callHeaders['x-product-id']).toBe('testapp');
    expect(callHeaders['Authorization']).toBe('Bearer test-token');
  });

  it('should throw on non-ok response', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 500,
      })
    );

    const client = createSubscriptionClient(baseConfig);
    await expect(client.getPlans()).rejects.toThrow('getPlans failed: 500');
  });
});
