import { Block } from "./Block";
import { Literal } from "./Literal";
import { Regex } from "./Regex";
import { Optional } from "./Optional";
import { Sequence } from "./Sequence";
import { Cursor } from "./Cursor";
import { Reference } from "./Reference";

describe("Block", () => {
  describe("simple blocks", () => {
    test("matches empty block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("{}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{}");
      expect(result.ast!.children.length).toBe(2);
      expect(result.ast!.children[0].value).toBe("{");
      expect(result.ast!.children[1].value).toBe("}");
    });

    test("matches block with content", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      const result = block.exec("{hello}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{hello}");
      expect(result.ast!.children.length).toBe(3);
      expect(result.ast!.children[0].value).toBe("{");
      expect(result.ast!.children[1].value).toBe("hello");
      expect(result.ast!.children[2].value).toBe("}");
    });

    test("null content skips inner text", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{stuff inside}");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.lastIndex).toBe(13);
      expect(node!.children.length).toBe(3);
      expect(node!.children[0].value).toBe("{");
      expect(node!.children[1].value).toBe("stuff inside");
      expect(node!.children[1].name).toBe("braces-content");
      expect(node!.children[2].value).toBe("}");
    });

    test("null content with empty block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("{}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.children.length).toBe(2);
    });

    test("null content with nested delimiters", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{ { } }");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.lastIndex).toBe(6);
      expect(node!.children.length).toBe(3);
      expect(node!.children[1].value).toBe(" { } ");
    });

    test("matches block with whitespace content", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "\\s+"),
        new Literal("close", "}")
      );
      const result = block.exec("{ }");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{ }");
    });

    test("matches parenthesized block", () => {
      const block = new Block(
        "parens",
        new Literal("open", "("),
        new Regex("content", "[^()]+"),
        new Literal("close", ")")
      );
      const result = block.exec("(hello)");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("(hello)");
    });

    test("matches block with multi-char delimiters", () => {
      const block = new Block(
        "begin-end",
        new Literal("open", "BEGIN"),
        new Regex("content", "[^BE]+"),
        new Literal("close", "END")
      );
      const result = block.exec("BEGIN stuff END");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("BEGIN stuff END");
    });
  });

  describe("nested blocks", () => {
    test("scan finds matching close past nested braces", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{ { } }");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.lastIndex).toBe(6);
    });

    test("counts nesting depth correctly", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{ { { } } }");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.lastIndex).toBe(10);
    });

    test("nested blocks with recursive content", () => {
      const innerBlock = new Block(
        "inner",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const ws = new Optional("ws", new Regex("whitespace", "\\s+"));
      const block = new Block(
        "outer",
        new Literal("open", "{"),
        new Sequence("content", [ws, innerBlock, ws]),
        new Literal("close", "}")
      );
      const result = block.exec("{ {} }");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{ {} }");
      const children = result.ast!.children;
      expect(children[0].value).toBe("{");
      expect(children[children.length - 1].value).toBe("}");
    });

    test("graceful close when opens outnumber closes", () => {
      // With null content, the scanner finds the matching close
      // and the block captures open + close only
      const block = new Block(
        "tag",
        new Literal("open", "<task>"),
        null,
        new Literal("close", "</task>")
      );
      const cursor = new Cursor("<task>stuff <task> more stuff</task>");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.children[0].value).toBe("<task>");
      expect(node!.children[node!.children.length - 1].value).toBe("</task>");
    });

    test("graceful close with multiple unmatched opens", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{ { { }");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.firstIndex).toBe(0);
      expect(node!.lastIndex).toBe(6);
    });

    test("still fails when no close delimiter exists at all", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{ no close here");
      const node = block.parse(cursor);
      expect(node).toBeNull();
    });

    test("adjacent blocks after first block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      const cursor = new Cursor("{a}{b}");
      const first = block.parse(cursor);
      expect(first).not.toBeNull();
      expect(first!.value).toBe("{a}");
      expect(cursor.index).toBe(2);

      cursor.next();
      const second = block.parse(cursor);
      expect(second).not.toBeNull();
      expect(second!.value).toBe("{b}");
    });
  });

  describe("error cases", () => {
    test("returns null for unmatched open", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("{");
      expect(result.ast).toBeNull();
    });

    test("returns null when input doesn't start with open", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("x{}");
      expect(result.ast).toBeNull();
    });

    test("returns null for empty input", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("");
      expect(result.ast).toBeNull();
    });
  });

  describe("content patterns", () => {
    test("matches sequence content pattern", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Sequence("content", [
          new Regex("name", "[a-z]+"),
          new Literal("colon", ":"),
          new Regex("value", "[a-z]+"),
        ]),
        new Literal("close", "}")
      );
      const result = block.exec("{foo:bar}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{foo:bar}");
      // open + sequence content + close
      expect(result.ast!.children.length).toBe(3);
      // The sequence node contains the individual parts
      const contentNode = result.ast!.children[1];
      expect(contentNode.children[0].value).toBe("foo");
      expect(contentNode.children[1].value).toBe(":");
      expect(contentNode.children[2].value).toBe("bar");
    });

    test("handles optional content pattern", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Optional("maybe", new Regex("content", "[a-z]+")),
        new Literal("close", "}")
      );
      const result = block.exec("{}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{}");
    });

    test("fails when required content doesn't match the text", () => {
      const block = new Block(
        "typed",
        new Literal("open", "{"),
        new Regex("digits", "[0-9]+"),
        new Literal("close", "}")
      );
      const result = block.exec("{abc}");
      expect(result.ast).toBeNull();
    });

    test("fails when content only partially fills the block", () => {
      const block = new Block(
        "typed",
        new Literal("open", "{"),
        new Regex("alpha", "[a-z]+"),
        new Literal("close", "}")
      );
      const result = block.exec("{abc 123}");
      expect(result.ast).toBeNull();
    });
  });

  describe("used with cursor directly", () => {
    test("parse advances cursor correctly", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{} rest");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.value).toBe("{}");
      expect(cursor.index).toBe(1);
    });

    test("parse restores cursor on failure", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("hello");
      const startIndex = cursor.index;
      block.parse(cursor);
      expect(cursor.index).toBe(startIndex);
    });
  });

  describe("clone", () => {
    test("clone creates independent copy", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      const cloned = block.clone("braces-copy") as Block;
      expect(cloned.name).toBe("braces-copy");
      expect(cloned.type).toBe("block");
      expect(cloned.id).toBe(block.id);
    });
  });

  describe("metadata", () => {
    test("has correct type", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      expect(block.type).toBe("block");
    });

    test("has correct name", () => {
      const block = new Block(
        "my-block",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      expect(block.name).toBe("my-block");
    });

    test("children includes open, content, and close patterns", () => {
      const content = new Regex("content", "[^{}]+");
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        content,
        new Literal("close", "}")
      );
      expect(block.children.length).toBe(3);
    });

    test("children without content has open and close only", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      expect(block.children.length).toBe(2);
    });

    test("getTokens returns open pattern tokens", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      expect(block.getTokens()).toEqual(["{"]);
    });

    test("test with other pattern", () => {
      const anyChar = new Regex("content", "[^{}]*");
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        anyChar,
        new Literal("close", "}")
      );
      const {ast} = block.exec("{text}");
      expect(ast?.value).toBe("{text}");
    });

    test("test with sequence content", () => {
      const anyChar = new Regex("content", "[^{}}]+");
      const refBlock = new Reference("braces");

      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Sequence("content", [anyChar, new Optional("ref", refBlock)]),
        new Literal("close", "}")
      );
      const {ast} = block.exec("{text{inner}}");

      expect(ast?.value).toBe("{text{inner}}");
    });
  });

  describe("recursive self-referencing block", () => {
    test("empty block as base case", () => {
      const ref = new Reference("block");
      const block = new Block(
        "block",
        new Literal("open", "{"),
        ref,
        new Literal("close", "}")
      );
      const result = block.exec("{}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{}");
      expect(result.ast!.children.length).toBe(2);
    });

    test("one level of nesting", () => {
      const ref = new Reference("block");
      const block = new Block(
        "block",
        new Literal("open", "{"),
        ref,
        new Literal("close", "}")
      );
      const result = block.exec("{{}}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{{}}");
      expect(result.ast!.children.length).toBe(3);
      expect(result.ast!.children[0].value).toBe("{");
      expect(result.ast!.children[1].value).toBe("{}");
      expect(result.ast!.children[2].value).toBe("}");
    });

    test("two levels of nesting", () => {
      const ref = new Reference("block");
      const block = new Block(
        "block",
        new Literal("open", "{"),
        ref,
        new Literal("close", "}")
      );
      const result = block.exec("{{{}}}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{{{}}}");
      const inner = result.ast!.children[1];
      expect(inner.value).toBe("{{}}");
      expect(inner.children[1].value).toBe("{}");
    });
  });

  describe("boolean test()", () => {
    test("returns true for matching block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      expect(block.test("{hello}")).toBe(true);
    });

    test("returns false for non-matching text", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      expect(block.test("no braces here")).toBe(false);
    });
  });

  describe("autocomplete introspection", () => {
    test("suggests content tokens after open delimiter", () => {
      const block = new Block(
        "config",
        new Literal("open", "{"),
        new Literal("keyword", "key"),
        new Literal("close", "}")
      );
      const open = block.children[0];
      const tokens = block.getTokensAfter(open);
      expect(tokens).toEqual(["key"]);
    });

    test("suggests close token after content", () => {
      const block = new Block(
        "config",
        new Literal("open", "{"),
        new Literal("keyword", "key"),
        new Literal("close", "}")
      );
      const content = block.children[1];
      const tokens = block.getTokensAfter(content);
      expect(tokens).toEqual(["}"]);
    });

    test("suggests both optional content and close after open", () => {
      const block = new Block(
        "maybe",
        new Literal("open", "{"),
        new Optional("maybe-digits", new Regex("digits", "[0-9]+")),
        new Literal("close", "}")
      );
      const open = block.children[0];
      const patterns = block.getPatternsAfter(open);
      expect(patterns.length).toBe(2);
      expect(patterns[0].type).toBe("optional");
      expect(patterns[1].name).toBe("close");
    });

    test("after close delimiter delegates to parent sequence", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      const semi = new Literal("semi", ";");
      const seq = new Sequence("statement", [block, semi]);

      const blockInSeq = seq.children[0];
      const close = blockInSeq.children[blockInSeq.children.length - 1];
      const patterns = blockInSeq.getPatternsAfter(close);
      expect(patterns.length).toBe(1);
      expect(patterns[0].name).toBe("semi");
    });

    test("block in sequence knows what comes next", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const semi = new Literal("semi", ";");
      const seq = new Sequence("statement", [block, semi]);

      const blockInSeq = seq.children[0];
      expect(blockInSeq.getNextTokens()).toEqual([";"]);
      expect(blockInSeq.getNextPatterns().length).toBe(1);
      expect(blockInSeq.getNextPatterns()[0].name).toBe("semi");
    });

    test("standalone block has no next tokens or patterns", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      expect(block.getNextTokens()).toEqual([]);
      expect(block.getNextPatterns()).toEqual([]);
    });

    test("getPatterns returns the open delimiter patterns", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const patterns = block.getPatterns();
      expect(patterns.length).toBe(1);
      expect(patterns[0].name).toBe("open");
    });

    test("returns empty for unknown child reference", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const stranger = new Literal("stranger", "x");
      expect(block.getPatternsAfter(stranger)).toEqual([]);
    });

    test("find locates a child pattern within the block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        new Regex("content", "[^{}]+"),
        new Literal("close", "}")
      );
      const found = block.find(p => p.name === "close");
      expect(found).not.toBeNull();
      expect(found!.type).toBe("literal");
    });
  });

  describe("equality", () => {
    test("blocks with identical structure are equal", () => {
      const a = new Block("braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}"));
      const b = new Block("braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}"));
      expect(a.isEqual(b)).toBe(true);
    });

    test("blocks with different delimiters are not equal", () => {
      const braces = new Block("braces", new Literal("open", "{"), null, new Literal("close", "}"));
      const parens = new Block("parens", new Literal("open", "("), null, new Literal("close", ")"));
      expect(braces.isEqual(parens)).toBe(false);
    });
  });

  describe("unicode and emoji", () => {
    test("wildcard block with emoji content", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const result = block.exec("{🔴🟢🔵}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{🔴🟢🔵}");
    });

    test("block with emoji delimiters", () => {
      const block = new Block(
        "stars",
        new Literal("open", "🌟"),
        new Regex("content", "[^🌟]+"),
        new Literal("close", "🌟")
      );
      const result = block.exec("🌟hello🌟");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("🌟hello🌟");
    });

    test("block with multi-char emoji delimiters", () => {
      const block = new Block(
        "arrows",
        new Literal("open", "➡️"),
        null,
        new Literal("close", "⬅️")
      );
      const result = block.exec("➡️content here⬅️");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("➡️content here⬅️");
    });

    test("nested blocks with emoji content between", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{🔴{🟢}🔵}");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.value).toBe("{🔴{🟢}🔵}");
    });

    test("block with combining characters in content", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      // e + combining acute = é
      const result = block.exec("{cafe\u0301}");
      expect(result.ast).not.toBeNull();
      expect(result.ast!.value).toBe("{cafe\u0301}");
    });

    test("cursor position correct after emoji block", () => {
      const block = new Block(
        "braces",
        new Literal("open", "{"),
        null,
        new Literal("close", "}")
      );
      const cursor = new Cursor("{🔴} rest");
      const node = block.parse(cursor);
      expect(node).not.toBeNull();
      expect(node!.value).toBe("{🔴}");
      // Cursor should be on the close delimiter
      expect(cursor.currentChar).toBe("}");
    });

    test("check text with more than one as open and close pattern", () => {
      const block = new Block(
        "braces",
        new Literal("open", "<script"),
        null,
        new Literal("close", "</script>")
      );
      const {ast, cursor} = block.exec("<script>Hello</script>");
      expect(ast).not.toBeNull();
      expect(ast!.value).toBe("<script>Hello</script>");
      // Cursor should be on the close delimiter
      expect(cursor.currentChar).toBe(">");
    });
  });
});
