import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { analyzerWorker } from './worker';
import * as dataAccess from './data-access';
import * as persistence from './persistence';
import * as pipelineModule from './pipeline';
import * as stage6Module from './stages/stage6-fingerprint';
import type { RawTicketBundle } from './preprocessor';

const FIXTURE = JSON.parse(
  readFileSync(
    resolve(__dirname, 'fixtures', 'T20260424.0045.input.json'),
    'utf8'
  )
) as RawTicketBundle;

let loadSpy: ReturnType<typeof vi.spyOn>;
let runPipelineSpy: ReturnType<typeof vi.spyOn>;
let insertSpy: ReturnType<typeof vi.spyOn>;
let completeSpy: ReturnType<typeof vi.spyOn>;
let failSpy: ReturnType<typeof vi.spyOn>;
let updateStatusSpy: ReturnType<typeof vi.spyOn>;
let bulkStageSpy: ReturnType<typeof vi.spyOn>;
let insertFailedSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
  loadSpy = vi.spyOn(dataAccess, 'loadTicketBundle');
  runPipelineSpy = vi.spyOn(pipelineModule, 'runPipeline');
  insertSpy = vi
    .spyOn(persistence, 'insertAnalysis')
    .mockResolvedValue({ id: 'an_uuid', analysis_version: 1 });
  completeSpy = vi.spyOn(persistence, 'completeJob').mockResolvedValue();
  failSpy = vi.spyOn(persistence, 'failJob').mockResolvedValue();
  updateStatusSpy = vi.spyOn(persistence, 'updateJobStatus').mockResolvedValue();
  bulkStageSpy = vi
    .spyOn(persistence, 'bulkInsertStageExecutions')
    .mockResolvedValue();
  insertFailedSpy = vi
    .spyOn(persistence, 'insertFailedAnalysis')
    .mockResolvedValue({ id: 'failed_uuid', analysis_version: 1 });
  vi.spyOn(persistence, 'updateAnalysisFingerprint').mockResolvedValue();
  // Default Stage 6 to a no-op success in tests; worker treats failures as
  // non-fatal anyway, so we just need it not to make real HTTP calls.
  vi.spyOn(stage6Module, 'runFingerprintStage').mockRejectedValue(
    new Error('fingerprint stub: tests do not exercise stage 6')
  );
});

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

describe('analyzerWorker.runJob', () => {
  it('writes the analysis row and marks the job complete on a happy-path run', async () => {
    loadSpy.mockResolvedValueOnce(FIXTURE);
    runPipelineSpy.mockResolvedValueOnce({
      outcome: 'complete',
      analysis: {
        summary: 's',
        timeline: [],
        what_was_done: [],
        what_should_have_been_done: [],
        gaps: [],
        next_step: 'next',
        next_step_rationale: 'why',
        post_resolution_analysis: null,
        confidence_score: 0.8,
        needs_human_review: false,
        human_review_reasons: [],
        ambiguities_for_opus: [],
        itglue_docs_referenced: [],
      },
      pre: {
        header: {
          ticket_number: 'T20260424.0045',
          autotask_ticket_id: 680282,
          title: 't',
          status_label: 'Waiting Customer',
          priority_label: 'Minor Service',
          queue: 'Level 2 Support',
          account_name: 'Seubert and Associates',
          contact_name: 'Tyler Lyster',
          contact_email: 'tlyster@seubert.com',
          created_at: '2026-04-24T12:53:50.163Z',
          resolved_at: null,
        },
        events: [],
        counts: {
          total_events: 0,
          customer_facing: 0,
          internal_only: 0,
          mixed: 0,
          filtered_noise: 8,
        },
        content_hash: 'a'.repeat(64),
      },
      meta: {
        haiku_used: true,
        sonnet_used: true,
        opus_used: false,
        total_input_tokens: 6_000,
        total_output_tokens: 800,
        total_cache_creation_tokens: 0,
        total_cache_read_tokens: 0,
        estimated_cost_usd: 0.05,
        cost_circuit_breaker_tripped: false,
      },
      filtered_noise_count: 8,
      itglue_search_used: false,
      itglue_org_resolved: false,
      triage_response: {} as never,
      sonnet_response: {} as never,
      opus_response: null,
      model_traces: {},
    });

    const result = await analyzerWorker.runJob(
      'job_1',
      'T20260424.0045',
      'user_abc'
    );

    expect(result.outcome).toBe('complete');
    expect(result.analysis_id).toBe('an_uuid');
    expect(insertSpy).toHaveBeenCalledTimes(1);
    expect(insertSpy.mock.calls[0][0]).toMatchObject({
      ticket_number: 'T20260424.0045',
      autotask_ticket_id: 680282,
      triggered_by_user_id: 'user_abc',
      status: 'complete',
      haiku_used: true,
      sonnet_used: true,
      opus_used: false,
      filtered_noise_count: 8,
    });
    expect(completeSpy).toHaveBeenCalledWith('job_1', 'an_uuid');
    expect(failSpy).not.toHaveBeenCalled();
  });

  it('completes the job pointing at the existing analysis on idempotent short-circuit', async () => {
    loadSpy.mockResolvedValueOnce(FIXTURE);
    runPipelineSpy.mockResolvedValueOnce({
      outcome: 'idempotent_short_circuit',
      existing_analysis_id: 'existing_id',
      existing_analysis_version: 4,
      pre: {
        header: {
          ticket_number: 'T20260424.0045',
          autotask_ticket_id: 680282,
          title: 't',
          status_label: 'x',
          priority_label: null,
          queue: null,
          account_name: null,
          contact_name: null,
          contact_email: null,
          created_at: '2026-04-24T12:53:50.163Z',
          resolved_at: null,
        },
        events: [],
        counts: {
          total_events: 0,
          customer_facing: 0,
          internal_only: 0,
          mixed: 0,
          filtered_noise: 0,
        },
        content_hash: 'b'.repeat(64),
      },
    });

    const result = await analyzerWorker.runJob('job_1', 'T20260424.0045', null);
    expect(result.outcome).toBe('idempotent_short_circuit');
    expect(result.analysis_id).toBe('existing_id');
    expect(insertSpy).not.toHaveBeenCalled();
    expect(completeSpy).toHaveBeenCalledWith('job_1', 'existing_id');
  });

  it('marks the job failed with a clear message when the ticket is missing', async () => {
    loadSpy.mockRejectedValueOnce(
      new dataAccess.TicketNotFoundError('T20260424.0045')
    );

    const result = await analyzerWorker.runJob(
      'job_1',
      'T20260424.0045',
      null
    );
    expect(result.outcome).toBe('failed');
    expect(result.analysis_id).toBeNull();
    expect(failSpy).toHaveBeenCalledTimes(1);
    expect(failSpy.mock.calls[0][1]).toMatch(/not found in local mirror/);
    expect(insertSpy).not.toHaveBeenCalled();
    expect(completeSpy).not.toHaveBeenCalled();
  });

  it('fails the job and persists the error message when the pipeline throws', async () => {
    loadSpy.mockResolvedValueOnce(FIXTURE);
    runPipelineSpy.mockRejectedValueOnce(new Error('LLM stage on claude-sonnet-4-6 failed twice'));

    const result = await analyzerWorker.runJob('job_1', 'T20260424.0045', null);
    expect(result.outcome).toBe('failed');
    expect(failSpy).toHaveBeenCalledTimes(1);
    expect(failSpy.mock.calls[0][1]).toMatch(/failed twice/);
  });

  it('drives updateJobStatus through the pipeline progress callbacks', async () => {
    loadSpy.mockResolvedValueOnce(FIXTURE);
    runPipelineSpy.mockImplementationOnce((async (
      _input: unknown,
      _deps: unknown,
      callbacks: { onStage?: (stage: string) => Promise<void> | void } | undefined
    ) => {
      await callbacks?.onStage?.('fetching');
      await callbacks?.onStage?.('triaging');
      await callbacks?.onStage?.('analyzing');
      return {
        outcome: 'complete',
        analysis: {
          summary: 's',
          timeline: [],
          what_was_done: [],
          what_should_have_been_done: [],
          gaps: [],
          next_step: 'n',
          next_step_rationale: 'r',
          post_resolution_analysis: null,
          confidence_score: 0.8,
          needs_human_review: false,
          human_review_reasons: [],
          ambiguities_for_opus: [],
          itglue_docs_referenced: [],
        },
        pre: {
          header: {
            ticket_number: 'T20260424.0045',
            autotask_ticket_id: 680282,
            title: 't',
            status_label: null,
            priority_label: null,
            queue: null,
            account_name: null,
            contact_name: null,
            contact_email: null,
            created_at: '2026-04-24T12:53:50.163Z',
            resolved_at: null,
          },
          events: [],
          counts: {
            total_events: 0,
            customer_facing: 0,
            internal_only: 0,
            mixed: 0,
            filtered_noise: 0,
          },
          content_hash: 'c'.repeat(64),
        },
        meta: {
          haiku_used: true,
          sonnet_used: true,
          opus_used: false,
          total_input_tokens: 0,
          total_output_tokens: 0,
          total_cache_creation_tokens: 0,
          total_cache_read_tokens: 0,
          estimated_cost_usd: 0,
          cost_circuit_breaker_tripped: false,
        },
        filtered_noise_count: 0,
        itglue_search_used: false,
        itglue_org_resolved: false,
        triage_response: {} as never,
        sonnet_response: {} as never,
        opus_response: null,
        model_traces: {},
      };
    }) as never);

    await analyzerWorker.runJob('job_1', 'T20260424.0045', null);

    const stagesPassedToUpdate = (updateStatusSpy.mock.calls as Array<[string, string]>).map(
      (c) => c[1]
    );
    expect(stagesPassedToUpdate).toEqual(['fetching', 'triaging', 'analyzing']);
  });
});
