import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
import {
  messagesToSimple, getSentCount, setSentCount, sentCountMap,
  afterTurn,
} from '../src/index.js';
import { saveEnv, resetEnv, stubEnv, createMockFetch, createMockOpencodeClient } from './helpers.js';
import { setOpencodeClient } from '../src/index.js';

beforeAll(() => saveEnv());

describe('TestMessagesToSimple', () => {
  it('converts user and assistant messages with text parts', () => {
    const msgs = [
      {
        info: { role: 'user', id: 'm1' },
        parts: [{ type: 'text', text: 'hello', id: 'p1' }],
      },
      {
        info: { role: 'assistant', id: 'm2' },
        parts: [{ type: 'text', text: 'world', id: 'p2' }],
      },
    ];
    const result = messagesToSimple(msgs as any);
    expect(result).toEqual([
      { role: 'user', content: 'hello' },
      { role: 'assistant', content: 'world' },
    ]);
  });

  it('filters out non-user/assistant roles', () => {
    const msgs = [
      {
        info: { role: 'system', id: 'm1' },
        parts: [{ type: 'text', text: 'sys msg', id: 'p1' }],
      },
    ];
    expect(messagesToSimple(msgs as any)).toEqual([]);
  });

  it('filters out messages with error field (mirrors isApiErrorMessage)', () => {
    const msgs = [
      {
        info: { role: 'user', id: 'm1', error: { name: 'APIError', data: { message: 'fail' } } },
        parts: [{ type: 'text', text: 'error msg', id: 'p1' }],
      },
    ];
    expect(messagesToSimple(msgs as any)).toEqual([]);
  });

  it('skips messages with no text content after extraction', () => {
    const msgs = [
      {
        info: { role: 'user', id: 'm1' },
        parts: [{ type: 'tool', callID: 'c1', tool: 'Bash', state: {}, id: 'p1' }],
      },
    ];
    expect(messagesToSimple(msgs as any)).toEqual([]);
  });

  it('joins multiple text parts with newline', () => {
    const msgs = [
      {
        info: { role: 'assistant', id: 'm1' },
        parts: [
          { type: 'text', text: 'part1', id: 'p1' },
          { type: 'text', text: 'part2', id: 'p2' },
        ],
      },
    ];
    const result = messagesToSimple(msgs as any);
    expect(result).toEqual([{ role: 'assistant', content: 'part1\npart2' }]);
  });
});

describe('TestOffsetReadWrite', () => {
  beforeEach(() => {
    sentCountMap.clear();
  });

  it('returns 0 for unknown session', () => {
    expect(getSentCount('unknown-session')).toBe(0);
  });

  it('writes then reads correctly', () => {
    setSentCount('sess-1', 5);
    expect(getSentCount('sess-1')).toBe(5);
  });

  it('updates existing value', () => {
    setSentCount('sess-1', 3);
    setSentCount('sess-1', 10);
    expect(getSentCount('sess-1')).toBe(10);
  });
});

describe('TestAfterTurn', () => {
  beforeEach(() => {
    resetEnv();
    sentCountMap.clear();
    vi.restoreAllMocks();
  });

  it('skips when no opencodeClient', async () => {
    setOpencodeClient(null);
    vi.spyOn(console, 'error').mockImplementation(() => {});
    await afterTurn('sess-1', 'Stop');
    expect(console.error).toHaveBeenCalled();
  });

  it('POSTs new messages beyond sentCount', async () => {
    stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
    const mockClient = createMockOpencodeClient();
    mockClient.session.messages.mockResolvedValue({
      data: [
        { info: { role: 'user', id: 'm1' }, parts: [{ type: 'text', text: 'hello', id: 'p1' }] },
        { info: { role: 'assistant', id: 'm2' }, parts: [{ type: 'text', text: 'world', id: 'p2' }] },
        { info: { role: 'user', id: 'm3' }, parts: [{ type: 'text', text: 'next', id: 'p3' }] },
      ],
      error: undefined,
    });
    setOpencodeClient(mockClient as any);

    vi.stubGlobal('fetch', createMockFetch({
      ok: true, status: 200,
      json: () => Promise.resolve({ ok: true }),
    }));

    await afterTurn('sess-1', 'Stop');
    expect(fetch).toHaveBeenCalled();
    expect(getSentCount('sess-1')).toBe(3);
  });

  it('does not POST when no new messages (sentCount equals total)', async () => {
    stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
    setSentCount('sess-1', 3);
    const mockClient = createMockOpencodeClient();
    mockClient.session.messages.mockResolvedValue({
      data: [
        { info: { role: 'user', id: 'm1' }, parts: [{ type: 'text', text: 'hello', id: 'p1' }] },
        { info: { role: 'assistant', id: 'm2' }, parts: [{ type: 'text', text: 'world', id: 'p2' }] },
        { info: { role: 'user', id: 'm3' }, parts: [{ type: 'text', text: 'next', id: 'p3' }] },
      ],
      error: undefined,
    });
    setOpencodeClient(mockClient as any);
    vi.stubGlobal('fetch', vi.fn());

    await afterTurn('sess-1', 'Stop');
    expect(fetch).not.toHaveBeenCalled();
  });

  it('does NOT update sentCount on POST failure', async () => {
    stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
    const mockClient = createMockOpencodeClient();
    mockClient.session.messages.mockResolvedValue({
      data: [{ info: { role: 'user', id: 'm1' }, parts: [{ type: 'text', text: 'hello', id: 'p1' }] }],
      error: undefined,
    });
    setOpencodeClient(mockClient as any);
    vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));

    await afterTurn('sess-1', 'Stop');
    expect(getSentCount('sess-1')).toBe(0);
  });

  it('sends hook_event_name in POST body', async () => {
    stubEnv({ OG_MEMORY_URL: 'http://localhost:8090' });
    const mockClient = createMockOpencodeClient();
    mockClient.session.messages.mockResolvedValue({
      data: [{ info: { role: 'user', id: 'm1' }, parts: [{ type: 'text', text: 'hello', id: 'p1' }] }],
      error: undefined,
    });
    setOpencodeClient(mockClient as any);
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true, status: 200,
      json: () => Promise.resolve({ ok: true }),
    });
    vi.stubGlobal('fetch', mockFetch);

    await afterTurn('sess-1', 'PreCompact');
    const callArgs = mockFetch.mock.calls[0]!;
    const body = JSON.parse(callArgs[1].body as string);
    expect(body.hook_event_name).toBe('PreCompact');
  });
});