import { describe, it, expect, vi, beforeEach } from 'vitest';
import jwt from 'jsonwebtoken';
import { ERROR_CODES } from '@insforge/shared-schemas';

const apiHost = 'https://cloud.test.insforge.dev';
const projectId = '77777777-7777-7777-7777-777777777777';
const jwtSecret = 's'.repeat(32);

vi.mock('../../src/infra/config/app.config', () => {
  const c = {
    cloud: { projectId, apiHost },
    app: { jwtSecret },
  };
  return {
    config: c,
    appConfig: c,
  };
});

interface MockAxiosError extends Error {
  __isAxiosError: true;
  response: { status: number; data: { error: string } };
}

// Axios mock factory — must be hoisted
const axiosGetMock = vi.fn();
const axiosDeleteMock = vi.fn();
const axiosPostMock = vi.fn();
const axiosIsAxiosError = vi.fn((err: unknown) => {
  return (err as { __isAxiosError?: boolean })?.__isAxiosError === true;
});

vi.mock('axios', () => {
  return {
    default: {
      get: axiosGetMock,
      delete: axiosDeleteMock,
      post: axiosPostMock,
      isAxiosError: axiosIsAxiosError,
    },
  };
});

function makeAxiosError(status: number): MockAxiosError {
  const err = new Error(`Request failed with status code ${status}`) as MockAxiosError;
  err.__isAxiosError = true;
  err.response = { status, data: { error: 'test' } };
  return err;
}

// Import after mocks are set up
const { PostHogProvider } = await import('../../src/providers/analytics/posthog.provider');

describe('PostHogProvider', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Reset singleton for each test
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (PostHogProvider as any).instance = undefined;
  });

  describe('getConnection', () => {
    it('signs JWT with projectId as sub and parses response', async () => {
      const responseData = {
        posthogProjectId: '12345',
        organizationName: 'Org',
        projectName: 'P',
        region: 'US',
        host: 'https://us.posthog.com',
        apiKey: 'phc_pub',
        status: 'active',
        createdAt: '2026-01-01T00:00:00Z',
      };

      axiosGetMock.mockResolvedValueOnce({ data: responseData });

      const out = await PostHogProvider.getInstance().getConnection();
      expect(out).not.toBeNull();
      expect(out!.posthogProjectId).toEqual('12345');
      expect(out!.apiKey).toEqual('phc_pub');

      // Verify the Authorization header was sent with a valid JWT
      const callArgs = axiosGetMock.mock.calls[0];
      const headers = callArgs[1].headers as Record<string, string>;
      expect(headers.Authorization).toMatch(/^Bearer /);
      const token = headers.Authorization.slice(7);
      const decoded = jwt.verify(token, jwtSecret) as jwt.JwtPayload;
      expect(decoded.sub).toEqual(projectId);
    });

    it('returns null on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));

      const out = await PostHogProvider.getInstance().getConnection();
      expect(out).toBeNull();
    });

    it('throws AppError with UPSTREAM_FAILURE on 502', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(502));

      await expect(PostHogProvider.getInstance().getConnection()).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getDashboards', () => {
    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));

      await expect(PostHogProvider.getInstance().getDashboards()).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosGetMock.mockRejectedValueOnce(new Error('Network Error'));

      await expect(PostHogProvider.getInstance().getDashboards()).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });

    it('parses dashboards response', async () => {
      const responseData = {
        dashboards: [
          {
            id: 1,
            name: 'Main Dashboard',
            url: 'https://us.posthog.com/dashboard/1',
          },
        ],
        count: 1,
      };
      axiosGetMock.mockResolvedValueOnce({ data: responseData });

      const out = await PostHogProvider.getInstance().getDashboards();
      expect(out.count).toEqual(1);
      expect(out.dashboards).toHaveLength(1);
      expect(out.dashboards[0].name).toEqual('Main Dashboard');
    });
  });

  describe('getSummary', () => {
    it('parses summary response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          todayEvents: 1234,
          dau24h: 56,
          totalEvents7d: 8000,
          topEvents: [{ event: 'pageview', count: 500 }],
        },
      });
      const out = await PostHogProvider.getInstance().getSummary();
      expect(out.todayEvents).toEqual(1234);
      expect(out.topEvents[0].event).toEqual('pageview');
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(PostHogProvider.getInstance().getSummary()).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosGetMock.mockRejectedValueOnce(new Error('Network Error'));
      await expect(PostHogProvider.getInstance().getSummary()).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getRecentEvents', () => {
    it('parses events response with custom limit', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          next: null,
          events: [
            { id: 'e1', event: 'pageview', distinctId: 'u1', timestamp: '2026-04-29T10:00:00Z' },
          ],
        },
      });
      const out = await PostHogProvider.getInstance().getRecentEvents(5);
      expect(out.events).toHaveLength(1);
      expect(out.events[0].event).toEqual('pageview');
      const call = axiosGetMock.mock.calls[0];
      expect(call[1].params.limit).toEqual(5);
    });

    it('default limit is 10', async () => {
      axiosGetMock.mockResolvedValueOnce({ data: { next: null, events: [] } });
      await PostHogProvider.getInstance().getRecentEvents();
      const call = axiosGetMock.mock.calls[0];
      expect(call[1].params.limit).toEqual(10);
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(PostHogProvider.getInstance().getRecentEvents()).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });
  });

  describe('disconnect', () => {
    it('issues DELETE to the correct URL', async () => {
      axiosDeleteMock.mockResolvedValueOnce({ status: 204 });

      await PostHogProvider.getInstance().disconnect();

      expect(axiosDeleteMock).toHaveBeenCalledOnce();
      const callArgs = axiosDeleteMock.mock.calls[0];
      expect(callArgs[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/connection`);
    });

    it('throws AppError with UPSTREAM_FAILURE on error', async () => {
      axiosDeleteMock.mockRejectedValueOnce(makeAxiosError(500));

      await expect(PostHogProvider.getInstance().disconnect()).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getWebOverview', () => {
    it('forwards timeframe and parses response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          items: [
            {
              key: 'visitors',
              value: 100,
              previous: 80,
              changeFromPreviousPct: 25,
              isIncreaseBad: false,
            },
          ],
        },
      });

      const out = await PostHogProvider.getInstance().getWebOverview('7d');
      expect(out.items).toHaveLength(1);
      expect(out.items[0].key).toEqual('visitors');

      const call = axiosGetMock.mock.calls[0];
      expect(call[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/web-overview`);
      expect(call[1].params.timeframe).toEqual('7d');
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(PostHogProvider.getInstance().getWebOverview('7d')).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosGetMock.mockRejectedValueOnce(new Error('Network Error'));
      await expect(PostHogProvider.getInstance().getWebOverview('7d')).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getWebStats', () => {
    it('forwards breakdown and timeframe and parses response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          rows: [{ breakdownValue: '/home', visitors: 50, views: 100, uiFillFraction: 1.0 }],
        },
      });

      const out = await PostHogProvider.getInstance().getWebStats('Page', '7d');
      expect(out.rows).toHaveLength(1);
      expect(out.rows[0].breakdownValue).toEqual('/home');

      const call = axiosGetMock.mock.calls[0];
      expect(call[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/web-stats`);
      expect(call[1].params).toEqual({ breakdown: 'Page', timeframe: '7d' });
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosGetMock.mockRejectedValueOnce(new Error('Network Error'));
      await expect(PostHogProvider.getInstance().getWebStats('Page', '7d')).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getTrends', () => {
    it('forwards metric and timeframe and parses response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          series: [
            { date: '2026-05-01', count: 10 },
            { date: '2026-05-02', count: 12 },
          ],
        },
      });

      const out = await PostHogProvider.getInstance().getTrends('visitors', '7d');
      expect(out.series).toHaveLength(2);

      const call = axiosGetMock.mock.calls[0];
      expect(call[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/trends`);
      expect(call[1].params).toEqual({ metric: 'visitors', timeframe: '7d' });
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(PostHogProvider.getInstance().getTrends('visitors', '7d')).rejects.toMatchObject(
        {
          statusCode: 404,
          code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
        }
      );
    });
  });

  describe('getRetention', () => {
    it('parses retention response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          rows: [
            {
              date: '2026-04-29',
              label: 'Week 0',
              values: [{ count: 100 }, { count: 50 }, { count: null }],
            },
          ],
        },
      });

      const out = await PostHogProvider.getInstance().getRetention();
      expect(out.rows).toHaveLength(1);
      expect(out.rows[0].values).toHaveLength(3);

      const call = axiosGetMock.mock.calls[0];
      expect(call[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/retention`);
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosGetMock.mockRejectedValueOnce(new Error('Network Error'));
      await expect(PostHogProvider.getInstance().getRetention()).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });

  describe('getRecordings', () => {
    it('forwards limit and parses response', async () => {
      axiosGetMock.mockResolvedValueOnce({
        data: {
          items: [
            {
              id: 'r1',
              distinctId: 'u1',
              durationSeconds: 60,
              startTime: '2026-05-01T00:00:00Z',
              endTime: '2026-05-01T00:01:00Z',
              startUrl: '/home',
              clickCount: 3,
              consoleErrorCount: 0,
            },
          ],
        },
      });

      const out = await PostHogProvider.getInstance().getRecordings(20);
      expect(out.items).toHaveLength(1);

      const call = axiosGetMock.mock.calls[0];
      expect(call[0]).toEqual(`${apiHost}/projects/v1/${projectId}/posthog/recordings`);
      expect(call[1].params.limit).toEqual(20);
    });

    it('default limit is 10', async () => {
      axiosGetMock.mockResolvedValueOnce({ data: { items: [] } });
      await PostHogProvider.getInstance().getRecordings();
      const call = axiosGetMock.mock.calls[0];
      expect(call[1].params.limit).toEqual(10);
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosGetMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(PostHogProvider.getInstance().getRecordings()).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });
  });

  describe('createRecordingShare', () => {
    it('POSTs to the correct URL with URL-encoded id and parses response', async () => {
      axiosPostMock.mockResolvedValueOnce({
        data: { embedUrl: 'https://us.posthog.com/embedded/abc' },
      });

      const out = await PostHogProvider.getInstance().createRecordingShare('rec id/1');
      expect(out.embedUrl).toEqual('https://us.posthog.com/embedded/abc');

      const call = axiosPostMock.mock.calls[0];
      expect(call[0]).toEqual(
        `${apiHost}/projects/v1/${projectId}/posthog/recordings/rec%20id%2F1/share`
      );
    });

    it('rejects non-posthog.com embedUrl as schema violation (502)', async () => {
      axiosPostMock.mockResolvedValueOnce({
        data: { embedUrl: 'https://evil.example.com/embedded/abc' },
      });

      await expect(
        PostHogProvider.getInstance().createRecordingShare('rec1')
      ).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });

    it('throws AppError with ANALYTICS_NOT_CONNECTED on 404', async () => {
      axiosPostMock.mockRejectedValueOnce(makeAxiosError(404));
      await expect(
        PostHogProvider.getInstance().createRecordingShare('rec1')
      ).rejects.toMatchObject({
        statusCode: 404,
        code: ERROR_CODES.ANALYTICS_NOT_CONNECTED,
      });
    });

    it('throws AppError with UPSTREAM_FAILURE on network error', async () => {
      axiosPostMock.mockRejectedValueOnce(new Error('Network Error'));
      await expect(
        PostHogProvider.getInstance().createRecordingShare('rec1')
      ).rejects.toMatchObject({
        statusCode: 502,
        code: ERROR_CODES.UPSTREAM_FAILURE,
      });
    });
  });
});
