import { describe, expect, it, vi } from 'vitest';
import { printCallOutput } from '../src/cli/output-utils.js';
import { createCallResult } from '../src/result-utils.js';

describe('printCallOutput format selection', () => {
  it.each([
    [
      'auto prefers json payloads when available',
      'auto',
      {
        content: [
          { type: 'text', text: 'fallback text' },
          { type: 'json', json: { source: 'json' } },
          { type: 'markdown', text: '# heading' },
        ],
      },
      (logged: unknown) => {
        expect(JSON.parse(String(logged))).toEqual({ source: 'json' });
      },
    ],
    [
      'auto prints the full structuredContent object instead of only its data field',
      'auto',
      {
        structuredContent: {
          status: 'error',
          summary: 'Failed to create base: name is required',
          data: {},
          meta: {},
        },
        content: [
          {
            type: 'text',
            text: '{"status":"error","summary":"Failed to create base: name is required","data":{},"meta":{}}',
          },
        ],
      },
      (logged: unknown) => {
        expect(JSON.parse(String(logged))).toEqual({
          status: 'error',
          summary: 'Failed to create base: name is required',
          data: {},
          meta: {},
        });
      },
    ],
    [
      'auto prints structuredContent nested inside a result wrapper',
      'auto',
      {
        result: {
          content: [
            {
              type: 'text',
              text: '{\n  "entities": [],\n  "relations": []\n}',
            },
          ],
          structuredContent: {
            entities: [],
            relations: [],
          },
        },
      },
      (logged: unknown) => {
        expect(JSON.parse(String(logged))).toEqual({
          entities: [],
          relations: [],
        });
      },
    ],
    [
      'text prefers text over markdown/json',
      'text',
      {
        content: [
          { type: 'text', text: 'plain text wins' },
          { type: 'markdown', text: '# heading' },
          { type: 'json', json: { source: 'json' } },
        ],
      },
      (logged: unknown) => {
        expect(logged).toBe('plain text wins\n# heading');
      },
    ],
    [
      'markdown prefers markdown content',
      'markdown',
      {
        content: [
          { type: 'text', text: 'plain text' },
          { type: 'markdown', text: '## markdown wins' },
        ],
      },
      (logged: unknown) => {
        expect(logged).toBe('## markdown wins');
      },
    ],
    [
      'json falls back to raw output when no JSON candidate exists',
      'json',
      'raw-only-string',
      (logged: unknown) => {
        expect(logged).toBe('"raw-only-string"');
      },
    ],
    [
      'json emits valid JSON for object raw fallback instead of inspect output',
      'json',
      { content: [{ type: 'text', text: 'no json here' }] },
      (logged: unknown) => {
        expect(JSON.parse(String(logged))).toEqual({ content: [{ type: 'text', text: 'no json here' }] });
      },
    ],
    [
      'json emits valid JSON for MCP error envelopes instead of inspect output',
      'json',
      { content: [{ type: 'text', text: 'MCP error -32602: Tool search not found' }], isError: true },
      (logged: unknown) => {
        expect(JSON.parse(String(logged))).toEqual({
          content: [{ type: 'text', text: 'MCP error -32602: Tool search not found' }],
          isError: true,
        });
      },
    ],
    [
      'json emits null for undefined raw fallback',
      'json',
      undefined,
      (logged: unknown) => {
        expect(logged).toBe('null');
      },
    ],
    [
      'json emits a JSON string when raw fallback is circular',
      'json',
      (() => {
        const circular: { self?: unknown } = {};
        circular.self = circular;
        return circular;
      })(),
      (logged: unknown) => {
        expect(typeof logged).toBe('string');
        expect(() => JSON.parse(String(logged))).not.toThrow();
      },
    ],
    [
      'raw prints inspect output even when json exists',
      'raw',
      { content: [{ type: 'json', json: { id: 1 } }] },
      (logged: unknown) => {
        expect(String(logged)).toContain("type: 'json'");
      },
    ],
    [
      'auto falls back to readable raw output for plain object payloads',
      'auto',
      { result: 'Available pages for facebook/react' },
      (logged: unknown) => {
        expect(String(logged)).toContain("result: 'Available pages for facebook/react'");
      },
    ],
  ] as const)('%s', (_name, format, raw, assertLogged) => {
    const wrapped = createCallResult(raw);
    const log = vi.spyOn(console, 'log').mockImplementation(() => {});

    try {
      printCallOutput(wrapped, raw, format);
      expect(log).toHaveBeenCalledTimes(1);
      const logged = log.mock.calls[0]?.[0];
      assertLogged(logged);
    } finally {
      log.mockRestore();
    }
  });
});

describe('printCallOutput raw output', () => {
  it('does not truncate long strings when printing raw output', () => {
    const longText = 'x'.repeat(15000);
    const log = vi.spyOn(console, 'log').mockImplementation(() => {});
    const raw = { t: longText };
    const wrapped = createCallResult(raw);

    try {
      printCallOutput(wrapped, raw, 'raw');

      expect(log).toHaveBeenCalledTimes(1);
      const logged = log.mock.calls[0]?.[0];
      expect(typeof logged).toBe('string');
      expect(logged).not.toContain('... 5000 more characters');
      expect(logged).toContain(longText.slice(-50));
    } finally {
      log.mockRestore();
    }
  });

  it('prints nested values beyond the default inspect depth', () => {
    const log = vi.spyOn(console, 'log').mockImplementation(() => {});
    const raw = {
      level1: {
        level2: {
          level3: {
            level4: {
              level5: {
                level6: {
                  leaf: 'done',
                },
              },
            },
          },
        },
      },
    };
    const wrapped = createCallResult(raw);

    try {
      printCallOutput(wrapped, raw, 'raw');

      expect(log).toHaveBeenCalledTimes(1);
      const logged = log.mock.calls[0]?.[0];
      expect(typeof logged).toBe('string');
      expect(logged).toContain("leaf: 'done'");
    } finally {
      log.mockRestore();
    }
  });
});
