import { describe, it, expect } from 'vitest';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { CompletionContext } from '@codemirror/autocomplete';
import { mustacheCompletionSource } from './mustache-completion';
import { availableVariables } from './alert-variables';

function makeContext(doc: string, pos: number): CompletionContext {
  const state = EditorState.create({ doc });
  return new CompletionContext(state, pos, true);
}

function applyCompletion(doc: string, pos: number, label: string): string {
  // Drives the `apply` callback against a real EditorView so we exercise the
  // same code path CodeMirror invokes when the user selects a completion.
  const host = document.createElement('div');
  const view = new EditorView({
    parent: host,
    state: EditorState.create({ doc }),
  });
  view.dispatch({ selection: { anchor: pos } });
  const source = mustacheCompletionSource(availableVariables('ROUTE_METRIC'));
  const result = source(new CompletionContext(view.state, pos, true))!;
  const option = result.options.find((o) => o.label === label);
  if (!option || typeof option.apply !== 'function') {
    view.destroy();
    throw new Error(`No completion with label ${label} (or missing apply)`);
  }
  option.apply(view, option, result.from, result.to);
  const out = view.state.doc.toString();
  view.destroy();
  return out;
}

describe('mustacheCompletionSource', () => {
  const source = mustacheCompletionSource(availableVariables('ROUTE_METRIC'));

  it('returns null outside a Mustache tag', () => {
    const ctx = makeContext('Hello world', 5);
    expect(source(ctx)).toBeNull();
  });

  it('offers completions right after {{', () => {
    const ctx = makeContext('Hello {{', 8);
    const result = source(ctx)!;
    expect(result).not.toBeNull();
    const paths = result.options.map((o) => o.label);
    expect(paths).toContain('env.slug');
    expect(paths).toContain('alert.firedAt');
  });

  it('narrows as user types', () => {
    const ctx = makeContext('{{ale', 5);
    const result = source(ctx)!;
    const paths = result.options.map((o) => o.label);
    expect(paths.every((p) => p.startsWith('ale'))).toBe(true);
    expect(paths).toContain('alert.firedAt');
    expect(paths).not.toContain('env.slug');
  });

  it('does not offer out-of-kind vars', () => {
    const ctx = makeContext('{{exchange', 10);
    const result = source(ctx)!;
    // ROUTE_METRIC does not include exchange.* — expect no exchange. completions
    expect(result.options).toHaveLength(0);
  });

  describe('apply', () => {
    it('closes the Mustache tag when no closing braces follow', () => {
      // Cursor at end of `{{ale`; completion closes with `}}`.
      expect(applyCompletion('{{ale', 5, 'alert.firedAt')).toBe('{{alert.firedAt}}');
    });

    it('does not duplicate `}}` that closeBrackets already inserted', () => {
      // closeBrackets auto-inserts `}}` on `{{`, so the typed state before
      // accepting a completion is usually `{{<prefix>}}` with the cursor
      // sitting right before the closing braces. Selecting a completion must
      // consume the existing `}}` rather than appending another pair.
      expect(applyCompletion('{{ale}}', 5, 'alert.firedAt')).toBe('{{alert.firedAt}}');
    });
  });
});
