import json
import subprocess
import textwrap
import unittest
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = (
    REPO_ROOT.parent / "skill_lib" / "skills" / "zhihu-navigate" / "scripts" /
    "open_creator_entry.js"
)


def run_open_creator_entry(*, body_text: str, selectors: dict[str, list[dict]]) -> dict:
    node_script = textwrap.dedent(
        f"""
        import fs from 'node:fs';
        import vm from 'node:vm';

        const scriptPath = {json.dumps(str(SCRIPT_PATH))};
        const selectorMap = {json.dumps(selectors, ensure_ascii=False)};
        const bodyText = {json.dumps(body_text, ensure_ascii=False)};
        const source = fs.readFileSync(scriptPath, 'utf8');

        function createNode(spec) {{
          const node = {{
            id: String(spec?.id ?? ''),
            tagName: String(spec?.tagName || 'DIV').toUpperCase(),
            textContent: String(spec?.textContent ?? ''),
            innerText: String(spec?.innerText ?? spec?.textContent ?? ''),
            href: String(spec?.href ?? ''),
            className: String(spec?.className ?? ''),
            role: String(spec?.role ?? ''),
            tabIndex: Number.isFinite(spec?.tabIndex) ? Number(spec.tabIndex) : -1,
            clicked: false,
            click() {{
              this.clicked = true;
            }},
            getAttribute(name) {{
              if (name === 'class') {{
                return this.className;
              }}
              if (name === 'role') {{
                return this.role;
              }}
              if (name === 'href') {{
                return this.href;
              }}
              if (name === 'tabindex' && this.tabIndex >= 0) {{
                return String(this.tabIndex);
              }}
              return '';
            }},
            getBoundingClientRect() {{
              return {{
                width: spec?.visible === false ? 0 : 120,
                height: spec?.visible === false ? 0 : 32,
              }};
            }},
            parentElement: null,
            closest(selector) {{
              let current = this;
              while (current) {{
                if (matchesSelector(current, selector)) {{
                  return current;
                }}
                current = current.parentElement;
              }}
              return null;
            }},
          }};
          return node;
        }}

        const created = new Map();
        const nodesById = new Map();
        function matchesSelector(node, selector) {{
          const parts = selector.split(',').map((part) => part.trim()).filter(Boolean);
          return parts.some((part) => {{
            if (part === 'a[href]') {{
              return node.tagName === 'A' && Boolean(node.href);
            }}
            if (part === 'button') {{
              return node.tagName === 'BUTTON';
            }}
            if (part === '[role=\"button\"]' || part === "[role='button']") {{
              return node.role === 'button';
            }}
            if (part === '[tabindex]') {{
              return node.tabIndex >= 0;
            }}
            if (part === 'div') {{
              return node.tagName === 'DIV';
            }}
            if (part === 'span') {{
              return node.tagName === 'SPAN';
            }}
            return false;
          }});
        }}
        function createNodeList(selector) {{
          const specs = selectorMap[selector] || [];
          return specs.map((spec, index) => {{
            const key = `${{selector}}#${{index}}`;
            if (!created.has(key)) {{
              const node = createNode(spec);
              created.set(key, node);
              if (node.id) {{
                nodesById.set(node.id, node);
              }}
            }}
            return created.get(key);
          }});
        }}

        for (const selector of Object.keys(selectorMap)) {{
          createNodeList(selector);
        }}
        for (const node of created.values()) {{
          const parentId = selectorMap && Object.values(selectorMap)
            .flat()
            .find((spec) => String(spec?.id ?? '') === node.id)?.parentId;
          if (parentId && nodesById.has(parentId)) {{
            node.parentElement = nodesById.get(parentId);
          }}
        }}

        const bodyNode = createNode({{ tagName: 'BODY', textContent: bodyText, innerText: bodyText }});
        const context = {{
          args: {{ desired_target: 'article_editor' }},
          location: {{ href: 'https://www.zhihu.com/creator' }},
          document: {{
            body: bodyNode,
            querySelector(selector) {{
              if (selector === 'body') {{
                return bodyNode;
              }}
              return createNodeList(selector)[0] || null;
            }},
            querySelectorAll(selector) {{
              return createNodeList(selector);
            }},
          }},
          console,
          JSON,
          Math,
          Number,
          Object,
          RegExp,
          Set,
          String,
          Array,
          Error,
        }};

        try {{
          const result = vm.runInNewContext(`(function(){{\\n${{source}}\\n}})()`, context);
          process.stdout.write(JSON.stringify({{ ok: true, result, created: Object.fromEntries(created) }}));
        }} catch (error) {{
          process.stdout.write(JSON.stringify({{
            ok: false,
            error: String(error && error.message ? error.message : error),
          }}));
          process.exitCode = 1;
        }}
        """
    )
    completed = subprocess.run(
        ["node", "--input-type=module", "-e", node_script],
        check=False,
        capture_output=True,
        text=True,
    )
    payload = json.loads(completed.stdout)
    if completed.returncode != 0:
        raise AssertionError(payload["error"])
    return payload


class SkillScriptZhihuNavigateTest(unittest.TestCase):
    def test_open_creator_entry_clicks_anchor_write_entry(self):
        payload = run_open_creator_entry(
            body_text="创作者中心 写文章",
            selectors={
                "a[href], button, [role='button']": [
                  {
                        "tagName": "a",
                        "textContent": "写文章",
                        "href": "https://zhuanlan.zhihu.com/write",
                    }
                ]
            },
        )

        self.assertEqual(payload["result"]["status"], "creator_entry_clicked")
        self.assertTrue(payload["created"]["a[href], button, [role='button']#0"]["clicked"])

    def test_open_creator_entry_clicks_button_write_entry(self):
        payload = run_open_creator_entry(
            body_text="创作者中心 发布内容",
            selectors={
                "a[href], button, [role='button']": [
                    {
                        "tagName": "button",
                        "textContent": "写文章",
                    }
                ]
            },
        )

        self.assertEqual(payload["result"]["status"], "creator_entry_clicked")
        self.assertTrue(payload["created"]["a[href], button, [role='button']#0"]["clicked"])

    def test_open_creator_entry_clicks_clickable_ancestor_for_nested_write_text(self):
        payload = run_open_creator_entry(
            body_text="创作者中心 写文章",
            selectors={
                "a[href], button, [role='button']": [],
                "div, span, [tabindex]": [
                    {
                        "id": "ancestor",
                        "tagName": "div",
                        "className": "creator-entry",
                        "tabIndex": 0,
                        "textContent": "",
                    },
                    {
                        "id": "label",
                        "parentId": "ancestor",
                        "tagName": "span",
                        "textContent": "写文章",
                    },
                ],
            },
        )

        self.assertEqual(payload["result"]["status"], "creator_entry_clicked")
        self.assertTrue(payload["created"]["div, span, [tabindex]#0"]["clicked"])


if __name__ == "__main__":
    unittest.main()
