import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import {
  preprocessTicket,
  tagTicketNote,
  tagTimeEntry,
  isWorkflowNoise,
  isEmailNotification,
  classifyActorType,
  computeContentHash,
  type RawTicketBundle,
} from './preprocessor';

// Load the regression fixture once for the file. The fixture is the canonical
// shape the data-access layer produces (header + ticket notes + time entries
// joined with creator name/email).
const FIXTURE_DIR = resolve(__dirname, 'fixtures');
const inputFixture = JSON.parse(
  readFileSync(`${FIXTURE_DIR}/T20260424.0045.input.json`, 'utf8')
) as RawTicketBundle;
const expectedFixture = JSON.parse(
  readFileSync(`${FIXTURE_DIR}/T20260424.0045.expected.json`, 'utf8')
) as {
  preprocessor: {
    filtered_workflow_noise_ids: number[];
    filtered_email_notification_ids_in_input: number[];
    retained_note_ids: number[];
    retained_time_entry_ids: number[];
  };
};

describe('isWorkflowNoise', () => {
  it('matches by Autotask Administrator resource id', () => {
    expect(
      isWorkflowNoise({
        id: 1,
        title: null,
        description: 'x',
        note_type: null,
        publish: null,
        creator_resource_id: 4,
        creator_name: 'Autotask Administrator',
        creator_email: null,
        creator_type: 1,
        create_date_time: '2026-04-24T00:00:00Z',
      })
    ).toBe(true);
  });

  it('matches by title prefix', () => {
    expect(
      isWorkflowNoise({
        id: 1,
        title: 'Workflow Rule "Foo" fired.',
        description: 'x',
        note_type: 13,
        publish: 1,
        creator_resource_id: 99,
        creator_name: null,
        creator_email: null,
        creator_type: null,
        create_date_time: '2026-04-24T00:00:00Z',
      })
    ).toBe(true);
  });

  it('does not match a real ticket note', () => {
    expect(
      isWorkflowNoise({
        id: 1,
        title: 'Wulf Support Ticket Update -',
        description: 'real content',
        note_type: 1,
        publish: 1,
        creator_resource_id: 29683311,
        creator_name: 'Lorentz Hinrichsen',
        creator_email: 'lorentz@wulfconsulting.com',
        creator_type: 1,
        create_date_time: '2026-04-24T00:00:00Z',
      })
    ).toBe(false);
  });
});

describe('isEmailNotification', () => {
  it('matches "Service Desk Notification" titles', () => {
    expect(
      isEmailNotification({
        id: 1,
        title: 'Service Desk Notification',
        description: 'a@b.com, c@d.com',
        note_type: 2,
        publish: 4,
        creator_resource_id: 99,
        creator_name: null,
        creator_email: null,
        creator_type: null,
        create_date_time: '2026-04-24T00:00:00Z',
      })
    ).toBe(true);
  });
  it('does not match other titles', () => {
    expect(
      isEmailNotification({
        id: 1,
        title: 'Wulf Support Ticket Update -',
        description: 'x',
        note_type: 1,
        publish: 1,
        creator_resource_id: 99,
        creator_name: null,
        creator_email: null,
        creator_type: null,
        create_date_time: '2026-04-24T00:00:00Z',
      })
    ).toBe(false);
  });
});

describe('classifyActorType', () => {
  it.each([
    ['lorentz@wulfconsulting.com', null, 'wulf_tech'],
    ['cory.houck@wulfconsulting.com', null, 'wulf_tech'],
    ['tlyster@seubert.com', null, 'client_contact'],
    ['rmurphy@vertafore.com', null, 'vendor'],
    [null, null, 'system'],
    [undefined, null, 'system'],
    [null, 4, 'automation'], // Autotask Administrator id
    ['anyone@anywhere.com', 4, 'automation'], // creator id beats domain
    ['mixed@WulfConsulting.COM', null, 'wulf_tech'], // case-insensitive
  ] as const)('email=%s creatorId=%s -> %s', (email, creatorId, expected) => {
    expect(classifyActorType(email, creatorId)).toBe(expected);
  });
});

describe('tagTicketNote', () => {
  const baseNote = {
    id: 1,
    title: 'Wulf Support Ticket Update -',
    description: 'hello',
    note_type: 1,
    creator_type: 1,
    create_date_time: '2026-04-24T14:34:46.583Z',
  };

  it('tags publish=1 as customer_facing and puts content in summary_notes', () => {
    const evt = tagTicketNote({
      ...baseNote,
      publish: 1,
      creator_resource_id: 29683311,
      creator_name: 'Lorentz Hinrichsen',
      creator_email: 'lorentz@wulfconsulting.com',
    });
    expect(evt).toMatchObject({
      source: 'ticket_note',
      visibility: 'customer_facing',
      actor: 'Lorentz Hinrichsen',
      actor_type: 'wulf_tech',
      summary_notes: 'hello',
    });
    expect(evt?.internal_notes).toBeUndefined();
  });

  it('tags publish=2 as internal_only and puts content in internal_notes', () => {
    const evt = tagTicketNote({
      ...baseNote,
      publish: 2,
      creator_resource_id: 29683311,
      creator_name: 'Lorentz Hinrichsen',
      creator_email: 'lorentz@wulfconsulting.com',
    });
    expect(evt).toMatchObject({
      visibility: 'internal_only',
      internal_notes: 'hello',
    });
    expect(evt?.summary_notes).toBeUndefined();
  });
});

describe('tagTimeEntry', () => {
  const baseEntry = {
    id: 1,
    resource_id: 30861463,
    resource_name: 'Cory Houck',
    resource_email: 'cory.houck@wulfconsulting.com',
    hours_worked: 0.5,
    entry_date: '2026-04-24T00:00:00Z',
    start_date_time: null,
    end_date_time: '2026-04-24T15:00:00Z',
    type: 2,
  };

  it('returns mixed visibility when both notes and internal_notes are present', () => {
    const evt = tagTimeEntry({
      ...baseEntry,
      notes: 'public summary',
      internal_notes: 'internal-only follow-up',
    });
    expect(evt).toMatchObject({
      source: 'time_entry',
      visibility: 'mixed',
      summary_notes: 'public summary',
      internal_notes: 'internal-only follow-up',
      hours: 0.5,
    });
  });

  it('returns customer_facing when only notes are present', () => {
    const evt = tagTimeEntry({ ...baseEntry, notes: 'public', internal_notes: null });
    expect(evt?.visibility).toBe('customer_facing');
    expect(evt?.summary_notes).toBe('public');
    expect(evt?.internal_notes).toBeUndefined();
  });

  it('returns internal_only when only internal_notes are present', () => {
    const evt = tagTimeEntry({ ...baseEntry, notes: null, internal_notes: 'internal' });
    expect(evt?.visibility).toBe('internal_only');
    expect(evt?.internal_notes).toBe('internal');
    expect(evt?.summary_notes).toBeUndefined();
  });

  it('drops entries with no narrative content', () => {
    expect(tagTimeEntry({ ...baseEntry, notes: null, internal_notes: null })).toBeNull();
    expect(tagTimeEntry({ ...baseEntry, notes: '   ', internal_notes: '' })).toBeNull();
  });
});

describe('computeContentHash', () => {
  const evt = {
    timestamp: '2026-04-24T12:53:50.163Z',
    actor: 'A',
    actor_type: 'wulf_tech' as const,
    source: 'ticket_create' as const,
    visibility: 'customer_facing' as const,
    summary_notes: 'x',
  };
  it('is deterministic for the same input', () => {
    expect(computeContentHash([evt], 7, 8, 99)).toBe(
      computeContentHash([evt], 7, 8, 99)
    );
  });
  it('changes when ticket status changes', () => {
    expect(computeContentHash([evt], 7, 8, 99)).not.toBe(
      computeContentHash([evt], 5, 8, 99)
    );
  });
  it('changes when priority changes', () => {
    expect(computeContentHash([evt], 7, 8, 99)).not.toBe(
      computeContentHash([evt], 7, 1, 99)
    );
  });
  it('is independent of object key insertion order', () => {
    const e1 = { ...evt };
    const e2 = {
      visibility: evt.visibility,
      summary_notes: evt.summary_notes,
      timestamp: evt.timestamp,
      actor_type: evt.actor_type,
      actor: evt.actor,
      source: evt.source,
    };
    expect(computeContentHash([e1], 7, 8, 99)).toBe(
      computeContentHash([e2], 7, 8, 99)
    );
  });
});

// =============================================================================
// Regression: end-to-end against the T20260424.0045 fixture.
// =============================================================================

describe('preprocessTicket — T20260424.0045 fixture', () => {
  const result = preprocessTicket(inputFixture);

  it('filters the four workflow-rule firings', () => {
    const filteredIds = expectedFixture.preprocessor.filtered_workflow_noise_ids;
    for (const id of filteredIds) {
      expect(result.events.some((e) => e.source === 'ticket_note' && e.actor !== 'Autotask Administrator' || false)).toBe(true);
      // Confirm none of the filtered IDs survived as events.
      const survived = inputFixture.notes
        .filter((n) => filteredIds.includes(n.id))
        .some((n) => result.events.some((e) => e.timestamp === n.create_date_time));
      expect(survived).toBe(false);
    }
  });

  it('filters all "Service Desk Notification" rows', () => {
    const filteredIds =
      expectedFixture.preprocessor.filtered_email_notification_ids_in_input;
    expect(filteredIds.length).toBeGreaterThan(0);
    const survived = inputFixture.notes
      .filter((n) => filteredIds.includes(n.id))
      .some((n) => result.events.some((e) => e.timestamp === n.create_date_time && e.source === 'ticket_note'));
    expect(survived).toBe(false);
  });

  it('reports filtered_noise = workflow + email-notification count', () => {
    const expected =
      expectedFixture.preprocessor.filtered_workflow_noise_ids.length +
      expectedFixture.preprocessor.filtered_email_notification_ids_in_input.length;
    expect(result.counts.filtered_noise).toBe(expected);
  });

  it("retains Lorentz's \"I'll take it from here\" note as a tagged event", () => {
    const lorentzNote = inputFixture.notes.find((n) => n.id === 33738796)!;
    expect(lorentzNote).toBeDefined();

    const matching = result.events.find(
      (e) => e.source === 'ticket_note' && e.timestamp === lorentzNote.create_date_time
    );
    expect(matching).toBeDefined();
    expect(matching).toMatchObject({
      actor: 'Lorentz Hinrichsen',
      actor_type: 'wulf_tech',
      visibility: 'customer_facing',
    });
    expect(matching?.summary_notes).toContain("take it from here");
  });

  it('emits 7 events total: 1 ticket_create + 1 retained note + 5 time entries', () => {
    expect(result.events.length).toBe(7);

    const counts = result.events.reduce<Record<string, number>>((acc, e) => {
      acc[e.source] = (acc[e.source] ?? 0) + 1;
      return acc;
    }, {});
    expect(counts).toEqual({
      ticket_create: 1,
      ticket_note: 1,
      time_entry: 5,
    });
  });

  it('produces a chronologically sorted timeline', () => {
    for (let i = 1; i < result.events.length; i++) {
      expect(
        result.events[i].timestamp >= result.events[i - 1].timestamp
      ).toBe(true);
    }
  });

  it('tags the ticket_create as client_contact based on contact email', () => {
    const created = result.events.find((e) => e.source === 'ticket_create');
    expect(created).toMatchObject({
      actor: 'Tyler Lyster',
      actor_type: 'client_contact',
      visibility: 'customer_facing',
    });
  });

  it('tags time entries 465933, 465961, 466183 as mixed visibility', () => {
    // These are the entries with both summary and internal notes per the fixture.
    const expectedMixed = new Set([465933, 465961, 466183]);
    for (const id of expectedMixed) {
      const entry = inputFixture.time_entries.find((e) => e.id === id)!;
      const evt = result.events.find(
        (e) => e.source === 'time_entry' && e.hours === entry.hours_worked && e.summary_notes && e.internal_notes
      );
      expect(evt, `time_entry ${id} should produce a mixed event`).toBeDefined();
      expect(evt?.visibility).toBe('mixed');
    }
  });

  it('reports counts that add up', () => {
    const { customer_facing, internal_only, mixed, total_events } = result.counts;
    expect(customer_facing + internal_only + mixed).toBe(total_events);
    expect(total_events).toBe(7);
    // 1 ticket_create + 1 ticket_note (publish=1) + 2 time_entries with only notes = 4 customer_facing
    expect(customer_facing).toBe(4);
    expect(internal_only).toBe(0);
    expect(mixed).toBe(3);
  });

  it('produces a stable content_hash across re-runs', () => {
    const a = preprocessTicket(inputFixture).content_hash;
    const b = preprocessTicket(inputFixture).content_hash;
    expect(a).toBe(b);
    expect(a).toMatch(/^[0-9a-f]{64}$/);
  });

  it('changes content_hash if a meaningful field changes', () => {
    const original = preprocessTicket(inputFixture).content_hash;
    const modified = preprocessTicket({
      ...inputFixture,
      ticket: { ...inputFixture.ticket, status: 5 },
    }).content_hash;
    expect(modified).not.toBe(original);
  });

  it('populates the LLM-bound header correctly', () => {
    expect(result.header).toMatchObject({
      ticket_number: 'T20260424.0045',
      autotask_ticket_id: 680282,
      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',
      resolved_at: null,
    });
  });
});
