import { describe, expect, it } from "vitest";

import { extractUcfsForReact } from "../react-adapter.js";
import type { UniversalComponentFact } from "../types.js";

const sourceCommit = "0123456789abcdef0123456789abcdef01234567";

function extractOne(filePath: string, source: string): UniversalComponentFact {
  const facts = extractUcfsForReact({
    inlineSources: [{ filePath, source }],
    sourceCommit,
    cwd: "/repo",
  });
  expect(facts.length).toBeGreaterThan(0);
  return facts[0];
}

function extractAll(files: Array<{ filePath: string; source: string }>) {
  return extractUcfsForReact({
    inlineSources: files,
    sourceCommit,
    cwd: "/repo",
  });
}

describe("extractUcfsForReact — definitionKind / wrappedBy", () => {
  it("classifies a plain function component as kind=function", () => {
    const ucf = extractOne(
      "/repo/Plain.tsx",
      `export function Plain(props: { label: string }) { return <div>{props.label}</div>; }`
    );
    expect(ucf.definitionKind).toBe("function");
    expect(ucf.wrappedBy).toEqual([]);
  });

  it("classifies forwardRef as kind=wrapper with wrappedBy", () => {
    const ucf = extractOne(
      "/repo/Card.tsx",
      `import { forwardRef } from 'react';
       export const Card = forwardRef<HTMLDivElement, { children?: any }>(
         (props, ref) => <div ref={ref}>{props.children}</div>
       );`
    );
    expect(ucf.definitionKind).toBe("wrapper");
    expect(ucf.wrappedBy).toContain("forwardRef");
  });

  it("classifies forwardRef(memo(...)) with wrapper chain", () => {
    const ucf = extractOne(
      "/repo/Wrapped.tsx",
      `import { forwardRef, memo } from 'react';
       export const Wrapped = forwardRef<HTMLDivElement, { x?: string }>(
         memo((props, ref) => <div ref={ref} />)
       );`
    );
    expect(ucf.definitionKind).toBe("wrapper");
    expect(ucf.wrappedBy[0]).toBe("forwardRef");
    expect(ucf.wrappedBy).toContain("memo");
  });
});

describe("extractUcfsForReact — imports", () => {
  it("captures imports with named bindings", () => {
    const ucf = extractOne(
      "/repo/UsesRadix.tsx",
      `import { Root, Trigger as DialogTrigger } from '@radix-ui/react-dialog';
       export function UsesRadix() { return <Root><DialogTrigger /></Root>; }`
    );
    const radix = ucf.imports.find((i) => i.moduleSpecifier === "@radix-ui/react-dialog");
    expect(radix).toBeTruthy();
    expect(radix!.resolvedPackage).toBe("@radix-ui/react-dialog");
    expect(radix!.importedNames).toContainEqual({ imported: "Root", local: "Root" });
    expect(radix!.importedNames).toContainEqual({ imported: "Trigger", local: "DialogTrigger" });
  });

  it("captures default and namespace imports", () => {
    const ucf = extractOne(
      "/repo/Mix.tsx",
      `import React from 'react';
       import * as Foo from './foo';
       export function Mix() { return React.createElement('div'); }`
    );
    const reactImport = ucf.imports.find((i) => i.moduleSpecifier === "react");
    expect(reactImport!.importedNames).toContainEqual({ imported: "default", local: "React" });

    const fooImport = ucf.imports.find((i) => i.moduleSpecifier === "./foo");
    expect(fooImport!.importedNames).toContainEqual({ imported: "*", local: "Foo" });
    expect(fooImport!.resolvedPackage).toBeUndefined();
  });

  it("returns empty imports for a component without any", () => {
    const ucf = extractOne(
      "/repo/NoImports.tsx",
      `export function NoImports() { return <div />; }`
    );
    expect(ucf.imports).toEqual([]);
  });
});

describe("extractUcfsForReact — rootElements / rootElementsTruncated", () => {
  it("extracts a single HTML root", () => {
    const ucf = extractOne(
      "/repo/Btn.tsx",
      `export function Btn() { return <button type="button">Click</button>; }`
    );
    expect(ucf.rootElements[0]).toMatchObject({ tag: "button", attrs: { type: "button" } });
    expect(ucf.rootElements[0].loc).toMatchObject({
      file: "/repo/Btn.tsx",
      line: 1,
    });
    expect(ucf.rootElementsTruncated).toBe(false);
  });

  it("emits two branches for ternary roots", () => {
    const ucf = extractOne(
      "/repo/TernaryBtn.tsx",
      `import { Slot } from '@radix-ui/react-slot';
       export function TernaryBtn({ asChild }: { asChild?: boolean }) {
         return asChild ? <Slot /> : <button />;
       }`
    );
    const tags = ucf.rootElements.map((r) => r.tag);
    expect(tags).toContain("Custom:Slot");
    expect(tags).toContain("button");
    expect(ucf.rootElements.every((r) => r.conditionalGuard)).toBe(true);
  });

  it("extracts inputType for <input>", () => {
    const ucf = extractOne(
      "/repo/Check.tsx",
      `export function Check() { return <input type="checkbox" />; }`
    );
    expect(ucf.rootElements[0].tag).toBe("input");
    expect(ucf.rootElements[0].inputType).toBe("checkbox");
  });

  it("truncates beyond 4 branches and flags rootElementsTruncated", () => {
    const ucf = extractOne(
      "/repo/Many.tsx",
      `export function Many({ a, b, c, d, e }: any) {
         return a ? <a /> : b ? <b /> : c ? <i /> : d ? <p /> : e ? <s /> : <div />;
       }`
    );
    expect(ucf.rootElements).toHaveLength(4);
    expect(ucf.rootElementsTruncated).toBe(true);
  });

  it("unwraps transparent context providers to the rendered primitive root", () => {
    const ucf = extractOne(
      "/repo/Tabs.tsx",
      `import * as React from 'react';
       import { Tabs as BaseTabs } from '@base-ui/react/tabs';
       const VariantContext = React.createContext('underline');
       export function Tabs(props: { value?: string }) {
         return (
           <VariantContext.Provider value="underline">
             <BaseTabs.Root value={props.value} />
           </VariantContext.Provider>
         );
       }`
    );
    expect(ucf.rootElements.map((root) => root.tag)).toEqual(["Custom:BaseTabs.Root"]);
  });

  it("unwraps generic containers around imported primitive roots", () => {
    const ucf = extractOne(
      "/repo/Select.tsx",
      `import * as React from 'react';
       import { Select as BaseSelect } from '@base-ui/react/select';
       const SelectContext = React.createContext(null);
       export function Select(props: { value?: string }) {
         return (
           <SelectContext.Provider value={null}>
             <div>
               <BaseSelect.Root value={props.value} />
             </div>
           </SelectContext.Provider>
         );
       }`
    );
    expect(ucf.rootElements.map((root) => root.tag)).toEqual(["Custom:BaseSelect.Root"]);
  });

  it("unwraps form-field wrappers to nested controls", () => {
    const ucf = extractOne(
      "/repo/Slider.tsx",
      `import { Field } from '@base-ui/react/field';
       import { Slider as BaseSlider } from '@base-ui/react/slider';
       export function Slider(props: { value?: number }) {
         return (
           <Field.Root>
             <BaseSlider.Root value={props.value} />
             <Field.Description>Help</Field.Description>
           </Field.Root>
         );
       }`
    );
    expect(ucf.rootElements.map((root) => root.tag)).toEqual(["Custom:BaseSlider.Root"]);
  });
});

describe("extractUcfsForReact — ariaRoles / ariaAttributes", () => {
  it("captures explicit role on root", () => {
    const ucf = extractOne(
      "/repo/Switchy.tsx",
      `export function Switchy() { return <button role="switch" aria-checked="true" />; }`
    );
    expect(ucf.ariaRoles).toContain("switch");
    expect(ucf.ariaAttributes["aria-checked"]).toBe("true");
  });

  it("returns empty when no role is set", () => {
    const ucf = extractOne("/repo/Plain2.tsx", `export function Plain2() { return <div />; }`);
    expect(ucf.ariaRoles).toEqual([]);
    expect(ucf.ariaAttributes).toEqual({});
  });
});

describe("extractUcfsForReact — props", () => {
  it("extracts props with type, optionality, defaults", () => {
    const ucf = extractOne(
      "/repo/PropsCmp.tsx",
      `interface Props { onClick: () => void; variant?: 'solid' | 'outline'; }
       export function PropsCmp({ variant = 'solid', onClick }: Props) {
         return <button onClick={onClick}>{variant}</button>;
       }`
    );
    const variant = ucf.props.find((p) => p.name === "variant");
    expect(variant).toBeTruthy();
    expect(variant!.optional).toBe(true);
    expect(variant!.hasDefault).toBe(true);
    expect(variant!.defaultValueText).toBe("'solid'");
    expect(variant!.isUnion).toBe(true);
    expect(variant!.loc).toMatchObject({
      file: "/repo/PropsCmp.tsx",
      line: 1,
    });

    const onClick = ucf.props.find((p) => p.name === "onClick");
    expect(onClick).toBeTruthy();
    expect(onClick!.optional).toBe(false);
    expect(onClick!.loc).toMatchObject({
      file: "/repo/PropsCmp.tsx",
      line: 1,
    });
  });

  it("returns empty props for a parameter-less component", () => {
    const ucf = extractOne("/repo/NoProps.tsx", `export function NoProps() { return <div />; }`);
    expect(ucf.props).toEqual([]);
  });
});

describe("extractUcfsForReact — compoundChildren", () => {
  it("detects Object.assign(Root, { Sub })", () => {
    const ucf = extractOne(
      "/repo/CardAssign.tsx",
      `function CardRoot(props: { children?: any }) { return <div>{props.children}</div>; }
       function Header() { return <h2 />; }
       export const Card = Object.assign(CardRoot, { Header });`
    );
    expect(ucf.compoundChildren).toContain("Header");
  });

  it("detects Root.Sub = ... assignment", () => {
    const ucf = extractOne(
      "/repo/CardAssign2.tsx",
      `export function Card(props: { children?: any }) { return <div>{props.children}</div>; }
       Card.Footer = function Footer() { return <footer />; };`
    );
    expect(ucf.compoundChildren).toContain("Footer");
  });

  it("returns empty compoundChildren for a simple component", () => {
    const ucf = extractOne("/repo/Simple.tsx", `export function Simple() { return <div />; }`);
    expect(ucf.compoundChildren).toEqual([]);
  });
});

describe("extractUcfsForReact — behavior hints", () => {
  it("flags hasInternalState when useState is called", () => {
    const ucf = extractOne(
      "/repo/Stateful.tsx",
      `import { useState } from 'react';
       export function Stateful() { const [v] = useState(0); return <div>{v}</div>; }`
    );
    expect(ucf.hasInternalState).toBe(true);
  });

  it("flags hasEffects when useEffect is called", () => {
    const ucf = extractOne(
      "/repo/EffectCmp.tsx",
      `import { useEffect } from 'react';
       export function EffectCmp() { useEffect(() => {}, []); return <div />; }`
    );
    expect(ucf.hasEffects).toBe(true);
  });

  it("infers isControlled=true when value+onChange present", () => {
    const ucf = extractOne(
      "/repo/Controlled.tsx",
      `interface Props { value: string; onChange: (v: string) => void; }
       export function Controlled({ value, onChange }: Props) {
         return <input value={value} onChange={(e) => onChange(e.target.value)} />;
       }`
    );
    expect(ucf.isControlled).toBe(true);
  });

  it("infers isControlled='unknown' when no value/onChange/default present", () => {
    const ucf = extractOne(
      "/repo/Stateless.tsx",
      `export function Stateless({ label }: { label: string }) { return <span>{label}</span>; }`
    );
    expect(ucf.isControlled).toBe("unknown");
  });
});

describe("extractUcfsForReact — styleSystem / classNamesUsed", () => {
  it("detects CSS Modules import", () => {
    const ucf = extractOne(
      "/repo/CssMod.tsx",
      `import styles from './CssMod.module.scss';
       export function CssMod() { return <div className={styles.root} />; }`
    );
    expect(ucf.styleSystem).toBe("cssModules");
  });

  it("detects styled-components import", () => {
    const ucf = extractOne(
      "/repo/Styled.tsx",
      `import styled from 'styled-components';
       const X = styled.div\`\`;
       export function Styled() { return <X />; }`
    );
    expect(ucf.styleSystem).toBe("styledComponents");
  });

  it("captures literal classNames and dedupes", () => {
    const ucf = extractOne(
      "/repo/ClassyCmp.tsx",
      `export function ClassyCmp() { return <button className="btn primary btn">x</button>; }`
    );
    expect(ucf.classNamesUsed).toContain("btn");
    expect(ucf.classNamesUsed).toContain("primary");
    expect(ucf.classNamesUsed.filter((c) => c === "btn")).toHaveLength(1);
  });

  it("returns unknown style system when no style import", () => {
    const ucf = extractOne("/repo/Unstyled.tsx", `export function Unstyled() { return <div />; }`);
    expect(ucf.styleSystem).toBe("unknown");
  });
});

describe("extractUcfsForReact — exports / exportedFromBarrel", () => {
  it("marks the primary export and detects barrel files", () => {
    const ucf = extractOne("/repo/index.tsx", `export function Foo() { return <div />; }`);
    expect(ucf.exportedFromBarrel).toBe(true);
    const fooExport = ucf.exports.find((e) => e.exportedAs === "Foo");
    expect(fooExport!.isPrimary).toBe(true);
  });

  it("does not mark non-index files as barrels", () => {
    const ucf = extractOne("/repo/Foo.tsx", `export function Foo() { return <div />; }`);
    expect(ucf.exportedFromBarrel).toBe(false);
  });

  it("extracts named default function components without renaming them to default", () => {
    const ucf = extractOne(
      "/repo/Button.tsx",
      `export default function Button() { return <button />; }`
    );

    expect(ucf.componentName).toBe("Button");
    expect(ucf.exports).toEqual([
      {
        exportedAs: "default",
        localName: "Button",
        isDefault: true,
        fromBarrel: false,
        isPrimary: true,
      },
    ]);
  });

  it("extracts identifier default exports when the target is a component", () => {
    const ucf = extractOne(
      "/repo/Button.tsx",
      `const Button = () => <button />; export default Button;`
    );

    expect(ucf.componentName).toBe("Button");
    expect(ucf.exports).toEqual([
      {
        exportedAs: "default",
        localName: "Button",
        isDefault: true,
        fromBarrel: false,
        isPrimary: true,
      },
    ]);
  });

  it("ignores default exports whose resolved target is not component-shaped", () => {
    const facts = extractUcfsForReact({
      inlineSources: [{ filePath: "/repo/config.tsx", source: `export default { gap: 4 };` }],
      sourceCommit,
      cwd: "/repo",
    });

    expect(facts).toEqual([]);
  });

  it("preserves aliased default exports from barrels", () => {
    const facts = extractAll([
      {
        filePath: "/repo/Button.tsx",
        source: `export default function Button() { return <button />; }`,
      },
      {
        filePath: "/repo/index.ts",
        source: `export { default as Button } from "./Button";`,
      },
    ]);

    const barrel = facts.find((fact) => fact.filePath === "index.ts");
    expect(barrel).toBeDefined();
    expect(barrel).toMatchObject({
      componentName: "Button",
      exportedFromBarrel: true,
      exports: [
        {
          exportedAs: "Button",
          localName: "default",
          isDefault: false,
          fromBarrel: true,
          isPrimary: true,
        },
      ],
    });
  });

  it("preserves named and star barrel exports", () => {
    const facts = extractAll([
      {
        filePath: "/repo/Button.tsx",
        source: `export function Button() { return <button />; }`,
      },
      {
        filePath: "/repo/Card.tsx",
        source: `export function Card() { return <section />; }`,
      },
      {
        filePath: "/repo/index.ts",
        source: `export { Button } from "./Button"; export * from "./Card";`,
      },
    ]);

    const barrelButton = facts.find(
      (fact) => fact.filePath === "index.ts" && fact.componentName === "Button"
    );
    const barrelCard = facts.find(
      (fact) => fact.filePath === "index.ts" && fact.componentName === "Card"
    );
    expect(barrelButton?.exports).toEqual([
      {
        exportedAs: "Button",
        localName: "Button",
        isDefault: false,
        fromBarrel: true,
        isPrimary: true,
      },
    ]);
    expect(barrelCard?.exports).toEqual([
      {
        exportedAs: "Card",
        localName: "Card",
        isDefault: false,
        fromBarrel: true,
        isPrimary: true,
      },
    ]);
  });
});

describe("extractUcfsForReact — identity & idempotence (§1, §2)", () => {
  const fixture = `import { forwardRef } from 'react';
    import { Slot } from '@radix-ui/react-slot';
    interface Props { asChild?: boolean; variant?: 'solid' | 'outline'; children?: any; }
    export const Button = forwardRef<HTMLButtonElement, Props>(
      ({ asChild, variant = 'solid', children, ...props }, ref) => {
        const Comp = asChild ? Slot : 'button';
        return asChild ? <Slot ref={ref as any}>{children}</Slot> : <button ref={ref}>{children}</button>;
      }
    );
    Button.Icon = function ButtonIcon({ children }: { children?: any }) { return <span>{children}</span>; };`;

  it("matches the §1 expected shape", () => {
    const ucf = extractOne("/repo/Button.tsx", fixture);
    expect(ucf.definitionKind).toBe("wrapper");
    expect(ucf.wrappedBy).toContain("forwardRef");
    expect(ucf.compoundChildren).toContain("Icon");
    const tags = ucf.rootElements.map((r) => r.tag);
    expect(tags).toContain("button");
    expect(tags).toContain("Custom:Slot");
    expect(ucf.rootElementsTruncated).toBe(false);
    const variant = ucf.props.find((p) => p.name === "variant");
    expect(variant!.defaultValueText).toBe("'solid'");
  });

  it("produces deep-equal UCFs across two adapter invocations on the same source", () => {
    const a = extractAll([{ filePath: "/repo/Button.tsx", source: fixture }]);
    const b = extractAll([{ filePath: "/repo/Button.tsx", source: fixture }]);
    expect(a.length).toBe(1);
    expect(b.length).toBe(1);
    // capturedAt is the only legitimate non-deterministic field; everything
    // else (id, props, rootElements, etc.) must match byte-for-byte.
    const stripCapturedAt = (u: UniversalComponentFact) => ({ ...u, capturedAt: "" });
    expect(stripCapturedAt(a[0])).toEqual(stripCapturedAt(b[0]));
    expect(a[0].id).toBe(b[0].id);
  });
});

describe("extractUcfsForReact — failure modes", () => {
  it("does not throw on malformed source — emits zero UCFs", () => {
    const facts = extractUcfsForReact({
      inlineSources: [{ filePath: "/repo/Broken.tsx", source: "export const = ;;;" }],
      sourceCommit,
      cwd: "/repo",
    });
    expect(facts).toEqual([]);
  });

  it("skips lowercase-named exports", () => {
    const facts = extractUcfsForReact({
      inlineSources: [
        {
          filePath: "/repo/util.tsx",
          source: `export const helper = () => 1; export function camelCase() { return null; }`,
        },
      ],
      sourceCommit,
      cwd: "/repo",
    });
    expect(facts).toEqual([]);
  });
});
