import { test } from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";

// Minimal vscode shim for the semantic reference provider.
class Position {
  constructor(line, character) {
    this.line = line;
    this.character = character;
  }
}
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
}
class Location {
  constructor(uri, range) {
    this.uri = uri;
    this.range = range;
  }
}
const Uri = {
  file: (p) => ({ fsPath: p }),
};

const require = createRequire(import.meta.url);
const Module = require("module");
const origResolve = Module._resolveFilename;
Module._resolveFilename = function (request, ...args) {
  if (request === "vscode") return "vscode-stub";
  return origResolve.call(this, request, ...args);
};
require.cache["vscode-stub"] = {
  id: "vscode-stub",
  filename: "vscode-stub",
  loaded: true,
  exports: {
    Position,
    Range,
    Location,
    Uri,
    SymbolKind: {},
    DocumentSymbol: class {},
  },
};

const { Ra3ReferenceProvider } = require("../out/features/navigation.js");
const { LineMap, parseXml } = require("../out/language/xmlParser.js");
const { extractIndexRecords } = require("../out/indexer/records.js");
const { buildReferenceIndex } = require("../out/indexer/referenceIndex.js");

const FILE = "C:/mod/Data/Units.xml";
const TEXT = `<AssetDeclaration>
  <GameObject id="Tank"/>
  <ObjectCreationList id="OCL">
    <CreateObject>
      <CreateObject>Tank</CreateObject>
    </CreateObject>
  </ObjectCreationList>
</AssetDeclaration>`;

function makeDocument(text = TEXT) {
  const lineStarts = [0];
  for (let i = 0; i < text.length; i++) {
    if (text.charCodeAt(i) === 10) lineStarts.push(i + 1);
  }
  return {
    uri: { fsPath: FILE },
    getText: () => text,
    offsetAt: (pos) => lineStarts[pos.line] + pos.character,
    positionAt: (offset) => {
      let lo = 0;
      let hi = lineStarts.length - 1;
      while (lo < hi) {
        const mid = (lo + hi + 1) >> 1;
        if (lineStarts[mid] <= offset) lo = mid;
        else hi = mid - 1;
      }
      return new Position(lo, offset - lineStarts[lo]);
    },
  };
}

function makeScope() {
  const parse = parseXml(TEXT);
  const lineMap = new LineMap(TEXT);
  const records = extractIndexRecords(parse, lineMap, TEXT);
  const def = {
    type: "GameObject",
    id: "Tank",
    file: FILE,
    line: 2,
    origin: "project",
  };
  const lookup = {
    assets: new Map([["GameObject", new Map([["tank", [def]]])]]),
    assetsById: new Map([["tank", [def]]]),
  };
  const references = buildReferenceIndex([{ file: FILE, records }], lookup);
  const idx = {
    ...lookup,
    references,
    complete: true,
    phase: "art",
    projectDir: "C:/mod",
    sdkDir: "C:/sdk",
    defines: new Map(),
    files: new Map(),
    streams: [],
    manifests: new Map(),
    sourceCandidates: [],
    diagnostics: [],
    stats: {},
  };
  return {
    merged: idx,
  };
}

function makeWs(scope) {
  const parse = parseXml(TEXT);
  const lineMap = new LineMap(TEXT);
  const indexer = {
    readDom: async (path) =>
      path === FILE
        ? { file: { path: FILE }, parse, lineMap, records: null }
        : null,
  };
  return {
    isRa3Workspace: () => true,
    getScope: async () => scope,
    indexer,
    indexerForFile: () => indexer,
    activeIndexer: () => indexer,
    recordsSyncSurfaceFor: () => ({
      get index() {
        return scope.merged;
      },
      invalidate: () => {},
      scheduleRebuild: () => {},
    }),
  };
}

test("semantic Find All References excludes the definition even when includeDeclaration is set", async () => {
  const scope = makeScope();
  const provider = new Ra3ReferenceProvider(makeWs(scope));
  const document = makeDocument();
  const defLine = TEXT.split("\n")[1];
  const defPos = new Position(1, defLine.indexOf('id="') + 4);

  const refs = await provider.provideReferences(document, defPos, {
    includeDeclaration: true,
  }, {});
  assert.ok(refs, "references are returned");
  assert.equal(refs.length, 1, "only the typed content reference is returned");
  assert.equal(refs[0].uri.fsPath, FILE);
  assert.equal(refs[0].range.start.line, 4);
  assert.equal(
    TEXT.split("\n")[4].slice(refs[0].range.start.character, refs[0].range.end.character),
    "Tank",
  );
});

test("FAR from the reference site itself returns the same result", async () => {
  const scope = makeScope();
  const provider = new Ra3ReferenceProvider(makeWs(scope));
  const document = makeDocument();
  const contentLine = TEXT.split("\n")[4];
  const contentPos = new Position(4, contentLine.indexOf("Tank") + 1);

  const refs = await provider.provideReferences(document, contentPos, {
    includeDeclaration: false,
  }, {});
  assert.equal(refs.length, 1);
  assert.equal(refs[0].range.start.line, 4);
});
