import Fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const repositoryMock = {
  saveNaturalLanguageQuery: vi.fn(),
  findClustersByProduct: vi.fn(),
  getLatestInsightForCluster: vi.fn(),
  getErrorClusterById: vi.fn(),
  findRelatedClusters: vi.fn(),
  createDiagnosticInsight: vi.fn(),
  updateErrorCluster: vi.fn(),
  updateInsightFeedback: vi.fn(),
  getActiveAlerts: vi.fn(),
  getTopErrorTypes: vi.fn(),
  getQueryHistory: vi.fn(),
  acknowledgeAlert: vi.fn(),
  resolveAlert: vi.fn(),
};

const queryParserMock = {
  parseQuery: vi.fn(),
  validateQuery: vi.fn(),
  generateQuerySuggestions: vi.fn(),
};

const queryExecutorMock = {
  executeQuery: vi.fn(),
};

const llmAnalyzerMock = {
  analyzeRootCause: vi.fn(),
  generatePatternSummary: vi.fn(),
};

const telemetryLinkingMock = {
  aggregateClusterContext: vi.fn(),
};

vi.mock('./repository.js', () => repositoryMock);
vi.mock('./query-parser.js', () => queryParserMock);
vi.mock('./query-executor.js', () => queryExecutorMock);
vi.mock('./llm-analyzer.js', () => llmAnalyzerMock);
vi.mock('./telemetry-linking.js', () => telemetryLinkingMock);

async function buildApp(payload?: { sub: string; productId?: string; role?: string }) {
  const aiDiagnosticsRoutes = (await import('./routes.js')).default;
  const app = Fastify({ logger: false });

  if (payload) {
    app.addHook('onRequest', async request => {
      request.jwtPayload = payload;
    });
  }

  await app.register(aiDiagnosticsRoutes, { prefix: '/api/ai-diagnostics' });
  return app;
}

describe('aiDiagnosticsRoutes', () => {
  beforeEach(() => {
    vi.clearAllMocks();

    queryParserMock.parseQuery.mockReturnValue({
      intent: 'root_cause',
      entities: {},
    });
    queryParserMock.validateQuery.mockReturnValue({
      valid: true,
      errors: [],
      warnings: [],
    });
    queryParserMock.generateQuerySuggestions.mockReturnValue([
      'Investigate iOS crash spikes',
      'Compare Android regressions',
    ]);

    queryExecutorMock.executeQuery.mockResolvedValue({
      aiResponse: 'Likely tied to the latest mobile release.',
      confidence: 0.87,
      supportingData: [{ type: 'cluster', id: 'ec_1', relevanceScore: 0.92 }],
      executionTimeMs: 42,
    });

    repositoryMock.saveNaturalLanguageQuery.mockResolvedValue(undefined);
    repositoryMock.findClustersByProduct.mockResolvedValue([]);
    repositoryMock.getLatestInsightForCluster.mockResolvedValue(null);
    repositoryMock.getErrorClusterById.mockResolvedValue(null);
    repositoryMock.findRelatedClusters.mockResolvedValue([]);
    repositoryMock.createDiagnosticInsight.mockResolvedValue(undefined);
    repositoryMock.updateErrorCluster.mockResolvedValue(undefined);
    repositoryMock.updateInsightFeedback.mockResolvedValue(undefined);
    repositoryMock.getActiveAlerts.mockResolvedValue([]);
    repositoryMock.getTopErrorTypes.mockResolvedValue([]);
    repositoryMock.getQueryHistory.mockResolvedValue([]);
    repositoryMock.acknowledgeAlert.mockResolvedValue(undefined);
    repositoryMock.resolveAlert.mockResolvedValue(undefined);

    telemetryLinkingMock.aggregateClusterContext.mockResolvedValue({
      contextSummary: 'Most events originated from iOS 18 devices.',
    });

    llmAnalyzerMock.analyzeRootCause.mockResolvedValue({
      analysisType: 'root_cause',
      generatedAt: '2026-03-21T12:00:00.000Z',
      rootCauseCategory: 'logic',
      hypothesis: 'A nil access happens after release gating is bypassed.',
      reasoning: 'The stack signature clusters around the same view transition.',
      confidence: 'high',
      confidenceScore: 0.93,
      evidence: [
        {
          type: 'stack_trace',
          description: 'Frames consistently point to the same code path.',
          strength: 'strong',
        },
      ],
      suggestedInvestigation: ['Inspect the view transition guard.'],
      potentialFixDirection: 'Reinstate the guard before state mutation.',
      similarResolvedIssues: [],
      feedbackStats: { helpful: 0, notHelpful: 0, engineerNotes: [] },
      modelUsed: 'gpt-4o-mini',
      promptTokens: 120,
      completionTokens: 60,
    });
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('rejects non-admin access', async () => {
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'member' });

    const res = await app.inject({
      method: 'GET',
      url: '/api/ai-diagnostics/query-history',
    });

    expect(res.statusCode).toBe(401);
    await app.close();
  });

  it('POST /ai-diagnostics/query executes and persists a parsed query', async () => {
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-diagnostics/query',
      payload: {
        query: 'Why did iOS crash yesterday?',
        productId: 'chronomind',
        timeRange: {
          start: '2026-03-20T00:00:00.000Z',
          end: '2026-03-21T00:00:00.000Z',
        },
      },
    });

    expect(res.statusCode).toBe(200);
    expect(queryParserMock.parseQuery).toHaveBeenCalledWith('Why did iOS crash yesterday?');
    expect(queryExecutorMock.executeQuery).toHaveBeenCalledWith(
      expect.objectContaining({
        entities: expect.objectContaining({
          products: ['chronomind'],
          timeRange: {
            start: '2026-03-20T00:00:00.000Z',
            end: '2026-03-21T00:00:00.000Z',
          },
        }),
      }),
      { productId: 'chronomind', userId: 'admin_1' }
    );
    expect(repositoryMock.saveNaturalLanguageQuery).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'admin_1',
        productId: 'chronomind',
        rawQuery: 'Why did iOS crash yesterday?',
        parsedIntent: 'root_cause',
      })
    );

    const body = res.json();
    expect(body.success).toBe(true);
    expect(body.result.aiResponse).toContain('Likely tied');

    await app.close();
  });

  it('POST /ai-diagnostics/query returns 400 when parsed query validation fails', async () => {
    queryParserMock.validateQuery.mockReturnValue({
      valid: false,
      errors: ['missing product scope'],
      warnings: [],
    });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-diagnostics/query',
      payload: {
        query: 'show me crashes',
      },
    });

    expect(res.statusCode).toBe(400);
    expect(queryExecutorMock.executeQuery).not.toHaveBeenCalled();
    expect(repositoryMock.saveNaturalLanguageQuery).not.toHaveBeenCalled();
    expect(res.json()).toEqual({
      error: 'Invalid query',
      details: ['missing product scope'],
    });

    await app.close();
  });

  it('GET /ai-diagnostics/clusters enriches clusters with latest insights when requested', async () => {
    repositoryMock.findClustersByProduct.mockResolvedValue([
      {
        id: 'ec_1',
        productId: 'lysnrai',
        fingerprintHash: 'fp_1',
        firstSeenAt: '2026-03-20T00:00:00.000Z',
        lastSeenAt: '2026-03-21T00:00:00.000Z',
        occurrenceCount: 10,
        uniqueUsers: 4,
        errorType: 'TypeError',
        messageTemplate: 'Cannot read property',
        stackSignature: 'stack-signature',
        relatedClusterIds: [],
        status: 'active',
        createdAt: '2026-03-20T00:00:00.000Z',
        updatedAt: '2026-03-21T00:00:00.000Z',
        ttl: 7776000,
      },
    ]);
    repositoryMock.getLatestInsightForCluster.mockResolvedValue({
      id: 'di_1',
      clusterId: 'ec_1',
      productId: 'lysnrai',
      analysisType: 'root_cause',
      generatedAt: '2026-03-21T12:00:00.000Z',
      rootCauseCategory: 'logic',
      hypothesis: 'State is stale during retry.',
      reasoning: 'All samples align to the same path.',
      confidence: 'high',
      confidenceScore: 0.9,
      evidence: [],
      suggestedInvestigation: [],
      feedbackStats: { helpful: 0, notHelpful: 0, engineerNotes: [] },
      modelUsed: 'gpt-4o-mini',
      promptTokens: 1,
      completionTokens: 1,
      createdAt: '2026-03-21T12:00:00.000Z',
      updatedAt: '2026-03-21T12:00:00.000Z',
      ttl: 7776000,
    });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'GET',
      url: '/api/ai-diagnostics/clusters?productId=lysnrai&includeInsights=true',
    });

    expect(res.statusCode).toBe(200);
    expect(repositoryMock.findClustersByProduct).toHaveBeenCalledWith('lysnrai', {
      status: undefined,
      limit: 50,
    });
    expect(repositoryMock.getLatestInsightForCluster).toHaveBeenCalledWith('ec_1', 'lysnrai');
    expect(res.json().clusters[0].latestInsight.id).toBe('di_1');

    await app.close();
  });

  it('POST /ai-diagnostics/clusters/:id/analyze stores a new insight and updates the cluster', async () => {
    repositoryMock.getErrorClusterById.mockResolvedValue({
      id: 'ec_1',
      productId: 'lysnrai',
      fingerprintHash: 'fp_1',
      firstSeenAt: '2026-03-20T00:00:00.000Z',
      lastSeenAt: '2026-03-21T00:00:00.000Z',
      occurrenceCount: 10,
      uniqueUsers: 4,
      errorType: 'TypeError',
      messageTemplate: 'Cannot read property',
      stackSignature: 'stack-signature',
      relatedClusterIds: [],
      status: 'active',
      createdAt: '2026-03-20T00:00:00.000Z',
      updatedAt: '2026-03-21T00:00:00.000Z',
      ttl: 7776000,
    });

    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-diagnostics/clusters/ec_1/analyze?productId=lysnrai',
      payload: {
        analysisType: 'root_cause',
        modelPreference: 'gpt-4o-mini',
      },
    });

    expect(res.statusCode).toBe(200);
    expect(telemetryLinkingMock.aggregateClusterContext).toHaveBeenCalledWith(
      expect.objectContaining({ id: 'ec_1' }),
      []
    );
    expect(llmAnalyzerMock.analyzeRootCause).toHaveBeenCalledWith(
      expect.objectContaining({
        cluster: expect.objectContaining({ id: 'ec_1' }),
        analysisType: 'root_cause',
      })
    );
    expect(repositoryMock.createDiagnosticInsight).toHaveBeenCalledWith(
      expect.objectContaining({
        clusterId: 'ec_1',
        productId: 'lysnrai',
        hypothesis: 'A nil access happens after release gating is bypassed.',
      })
    );
    expect(repositoryMock.updateErrorCluster).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 'ec_1',
        insightId: expect.stringMatching(/^di_/),
      })
    );

    await app.close();
  });

  it('POST /ai-diagnostics/alerts/:id/acknowledge records the admin user and note', async () => {
    const app = await buildApp({ sub: 'admin_2', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/ai-diagnostics/alerts/pa_1/acknowledge',
      payload: {
        note: 'Assigned to the mobile on-call engineer.',
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repositoryMock.acknowledgeAlert).toHaveBeenCalledWith('pa_1', {
      acknowledgedBy: 'admin_2',
      note: 'Assigned to the mobile on-call engineer.',
    });
    expect(res.json()).toEqual({ success: true, message: 'Alert acknowledged' });

    await app.close();
  });
});
