import { homedir } from "node:os";
import { describe, expect, it } from "vitest";
import { getParser, type TSNode } from "#src/access-intent/bash/parser";
import { resolvePlainVariableExpansion } from "#src/access-intent/bash/shell-variable-expansion";
import { makeTSNode } from "#test/helpers/fake-ts-node";

/** `$NAME` as tree-sitter-bash builds it: a `$` delimiter plus the name. */
function simpleExpansion(name: string): TSNode {
  return makeTSNode("simple_expansion", `$${name}`, [
    makeTSNode("$", "$"),
    makeTSNode("variable_name", name),
  ]);
}

/** `${NAME}` as tree-sitter-bash builds it: brace delimiters plus the name. */
function bracedExpansion(name: string): TSNode {
  return makeTSNode("expansion", `\${${name}}`, [
    makeTSNode("${", "${"),
    makeTSNode("variable_name", name),
    makeTSNode("}", "}"),
  ]);
}

function findNodeOfType(node: TSNode, type: string): TSNode | null {
  if (node.type === type) return node;
  for (let i = 0; i < node.childCount; i++) {
    const child = node.child(i);
    const found = child ? findNodeOfType(child, type) : null;
    if (found) return found;
  }
  return null;
}

describe("resolvePlainVariableExpansion", () => {
  describe("resolvable variables", () => {
    it("resolves $HOME to the OS home directory", () => {
      expect(resolvePlainVariableExpansion(simpleExpansion("HOME"))).toBe(
        homedir(),
      );
    });

    it("resolves ${HOME} to the OS home directory", () => {
      expect(resolvePlainVariableExpansion(bracedExpansion("HOME"))).toBe(
        homedir(),
      );
    });

    it("resolves $PWD to the base-relative marker", () => {
      // The shell's working directory is the projection's effective base, so
      // the base-relative form resolves correctly after any `cd` folding
      // without threading a base into this pure function.
      expect(resolvePlainVariableExpansion(simpleExpansion("PWD"))).toBe(".");
    });

    it("resolves ${PWD} to the base-relative marker", () => {
      expect(resolvePlainVariableExpansion(bracedExpansion("PWD"))).toBe(".");
    });
  });

  describe("variables outside the resolvable set", () => {
    it.each([
      "HOMEDIR",
      "CURRENT",
      "PATH",
      "PWDX",
      "TMPDIR",
    ])("leaves $%s unresolved", (name) => {
      expect(resolvePlainVariableExpansion(simpleExpansion(name))).toBeNull();
      expect(resolvePlainVariableExpansion(bracedExpansion(name))).toBeNull();
    });
  });

  describe("expansions carrying an operator", () => {
    it("leaves ${HOME:-/tmp} unresolved", () => {
      const node = makeTSNode("expansion", "${HOME:-/tmp}", [
        makeTSNode("${", "${"),
        makeTSNode("variable_name", "HOME"),
        makeTSNode(":-", ":-"),
        makeTSNode("word", "/tmp"),
        makeTSNode("}", "}"),
      ]);
      expect(resolvePlainVariableExpansion(node)).toBeNull();
    });

    it("leaves ${#HOME} unresolved", () => {
      const node = makeTSNode("expansion", "${#HOME}", [
        makeTSNode("${", "${"),
        makeTSNode("#", "#"),
        makeTSNode("variable_name", "HOME"),
        makeTSNode("}", "}"),
      ]);
      expect(resolvePlainVariableExpansion(node)).toBeNull();
    });
  });

  describe("nodes that are not a plain variable reference", () => {
    it("returns null for a node with no children", () => {
      expect(
        resolvePlainVariableExpansion(makeTSNode("simple_expansion", "$HOME")),
      ).toBeNull();
    });

    it("returns null for a node with no variable_name child", () => {
      const node = makeTSNode("expansion", "${}", [
        makeTSNode("${", "${"),
        makeTSNode("}", "}"),
      ]);
      expect(resolvePlainVariableExpansion(node)).toBeNull();
    });

    it("returns null for a variable_assignment naming a resolvable variable", () => {
      // `HOME=/tmp` binds the name; it is not a reference to its value.
      const node = makeTSNode("variable_assignment", "HOME=/tmp", [
        makeTSNode("variable_name", "HOME"),
        makeTSNode("=", "="),
        makeTSNode("word", "/tmp"),
      ]);
      expect(resolvePlainVariableExpansion(node)).toBeNull();
    });
  });

  describe("fidelity to the shapes tree-sitter-bash actually produces", () => {
    it.each([
      ["ls $HOME", "simple_expansion", homedir()],
      ["ls ${HOME}", "expansion", homedir()],
      ["ls $PWD", "simple_expansion", "."],
      ["ls ${PWD}", "expansion", "."],
      ["ls ${HOME:-/tmp}", "expansion", null],
      ["ls ${#HOME}", "expansion", null],
      ["ls $HOMEDIR", "simple_expansion", null],
    ])("resolves %s to %s", async (command, nodeType, expected) => {
      const parser = await getParser();
      const tree = parser.parse(command);
      expect(tree).not.toBeNull();
      if (!tree) return;
      try {
        const node = findNodeOfType(tree.rootNode, nodeType);
        expect(node).not.toBeNull();
        if (!node) return;
        expect(resolvePlainVariableExpansion(node)).toBe(expected);
      } finally {
        tree.delete();
      }
    });
  });
});
