/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import * as assert from 'assert';
import { describe, it } from 'node:test';
import { extractItems, extractJson, parsePartialJson } from '../src/extract';

describe('extract', () => {
  describe('extractItems', () => {
    interface TestStep {
      chunk: string;
      want: unknown[];
    }

    interface TestCase {
      name: string;
      steps: TestStep[];
    }

    const testCases: TestCase[] = [
      {
        name: 'handles simple array in chunks',
        steps: [
          { chunk: '[', want: [] },
          { chunk: '{"a": 1},', want: [{ a: 1 }] },
          { chunk: '{"b": 2}', want: [{ b: 2 }] },
          { chunk: ']', want: [] },
        ],
      },
      {
        name: 'handles nested objects',
        steps: [
          { chunk: '[{"outer": {', want: [] },
          {
            chunk: '"inner": "value"}},',
            want: [{ outer: { inner: 'value' } }],
          },
          { chunk: '{"next": true}]', want: [{ next: true }] },
        ],
      },
      {
        name: 'handles escaped characters',
        steps: [
          { chunk: '[{"text": "line1\\n', want: [] },
          { chunk: 'line2"},', want: [{ text: 'line1\nline2' }] },
          { chunk: '{"text": "tab\\there"}]', want: [{ text: 'tab\there' }] },
        ],
      },
      {
        name: 'ignores content before first array',
        steps: [
          { chunk: 'Here is an array:\n```json\n\n[', want: [] },
          { chunk: '{"a": 1},', want: [{ a: 1 }] },
          { chunk: '{"b": 2}]\n```\nDid you like my array?', want: [{ b: 2 }] },
        ],
      },
      {
        name: 'handles whitespace',
        steps: [
          { chunk: '[\n  ', want: [] },
          { chunk: '{"a": 1},\n  ', want: [{ a: 1 }] },
          { chunk: '{"b": 2}\n]', want: [{ b: 2 }] },
        ],
      },
    ];

    for (const tc of testCases) {
      it(tc.name, () => {
        let text = '';
        let cursor = 0;

        for (const step of tc.steps) {
          text += step.chunk;
          const result = extractItems(text, cursor);
          assert.deepStrictEqual(result.items, step.want);
          cursor = result.cursor;
        }
      });
    }
  });

  describe('extractJson', () => {
    interface TestCase {
      name: string;
      input: {
        text: string;
        throwOnBadJson?: boolean;
      };
      expected?: unknown;
      throws?: boolean;
    }

    const testCases: TestCase[] = [
      {
        name: 'extracts simple object',
        input: {
          text: 'prefix{"a":1}suffix',
        },
        expected: { a: 1 },
      },
      {
        name: 'extracts simple array',
        input: {
          text: 'prefix[1,2,3]suffix',
        },
        expected: [1, 2, 3],
      },
      {
        name: 'handles nested structures',
        input: {
          text: 'text{"a":{"b":[1,2]}}more',
        },
        expected: { a: { b: [1, 2] } },
      },
      {
        name: 'handles strings with braces',
        input: {
          text: '{"text": "not {a} json"}',
        },
        expected: { text: 'not {a} json' },
      },
      {
        name: 'returns null for invalid JSON without throw',
        input: {
          text: 'not json at all',
        },
        expected: null,
      },
      {
        name: 'throws for invalid JSON with throw flag',
        input: {
          text: 'not json at all',
          throwOnBadJson: true,
        },
        throws: true,
      },
    ];

    for (const tc of testCases) {
      it(tc.name, () => {
        if (tc.throws) {
          assert.throws(() => {
            extractJson(tc.input.text, true);
          });
        } else {
          const result = extractJson(
            tc.input.text,
            (tc.input.throwOnBadJson || false) as any
          );
          assert.deepStrictEqual(result, tc.expected);
        }
      });
    }
  });

  describe('parsePartialJson', () => {
    interface TestCase {
      name: string;
      input: string;
      expected: unknown;
    }

    const testCases: TestCase[] = [
      {
        name: 'parses complete object',
        input: '{"a":1,"b":2}',
        expected: { a: 1, b: 2 },
      },
      {
        name: 'parses partial object',
        input: '{"a":1,"b":',
        expected: { a: 1 },
      },
      {
        name: 'parses partial array',
        input: '[1,2,3,',
        expected: [1, 2, 3],
      },
      {
        name: 'parses nested partial structures',
        input: '{"a":{"b":1,"c":]}}',
        expected: { a: { b: 1 } },
      },
    ];

    for (const tc of testCases) {
      it(tc.name, () => {
        const result = parsePartialJson(tc.input);
        assert.deepStrictEqual(result, tc.expected);
      });
    }
  });
});
