import { ok, strictEqual } from "assert";
import { describe, expect, it } from "vitest";
import { SyntaxKind } from "../../src/core/types.js";
import { Model } from "../../src/index.js";
import { expectDiagnostics, extractSquiggles, t } from "../../src/testing/index.js";
import { Tester } from "../tester.js";

describe("compiler: intersections", () => {
  it("intersect 2 models", async () => {
    const { prop } = await Tester.compile(t.code`
      model Foo {
        ${t.modelProperty("prop")}: {a: string} & {b: string};
      }
    `);

    const propType = prop.type;
    strictEqual(propType.kind, "Model");
    strictEqual(propType.properties.size, 2);
    ok(propType.properties.has("a"));
    ok(propType.properties.has("b"));
  });

  it("keeps reference to source model in sourceModels", async () => {
    const { A, B, intersection, pos } = await Tester.compile(t.code`
      model ${t.model("A")} { one: string }
      model ${t.model("B")} { two: string }
      alias ${t.model("intersection")} = /*ASource*/A & /*BSource*/B;
      `);
    strictEqual(intersection.kind, "Model");
    expect(intersection.sourceModels).toHaveLength(2);
    strictEqual(intersection.sourceModels[0].model, A);
    strictEqual(intersection.sourceModels[0].usage, "intersection");
    strictEqual(intersection.sourceModels[0].node?.kind, SyntaxKind.TypeReference);
    strictEqual(intersection.sourceModels[0].node.pos, pos.ASource.pos);
    strictEqual(intersection.sourceModels[1].model, B);
    strictEqual(intersection.sourceModels[1].usage, "intersection");
    strictEqual(intersection.sourceModels[1].node?.kind, SyntaxKind.TypeReference);
    strictEqual(intersection.sourceModels[1].node.pos, pos.BSource.pos);
  });

  it("intersection type belong to namespace it is declared in", async () => {
    const { Foo } = await Tester.compile(t.code`
      namespace A {
        model ModelA {name: string}
      }
      namespace B {
        model ModelB {age: int32}
      }
      namespace C {
        model ${t.model("Foo")} {
          prop: A.ModelA & B.ModelB;
        }
      }
    `);

    const prop = Foo.properties.get("prop")!.type as Model;
    strictEqual(prop.kind, "Model");
    ok(prop.namespace);
    strictEqual(prop.namespace.name, "C");
  });

  it("allow intersections of template params", async () => {
    const { Foo } = await Tester.compile(t.code`
      model Bar<A, B> {
        prop: A & B;
      }
      model ${t.model("Foo")} {
        prop: Bar<{a: string}, {b: string}>;
      }
    `);

    const Bar = Foo.properties.get("prop")!.type as Model;
    const prop = Bar.properties.get("prop")!.type as Model;
    strictEqual(prop.kind, "Model");
    strictEqual(prop.properties.size, 2);
    ok(prop.properties.has("a"));
    ok(prop.properties.has("b"));
  });

  it("emit diagnostic if one of the intersected type is not a model", async () => {
    const { source, pos, end } = extractSquiggles(`
      model Foo {
        prop: {a: string} & ~~~"string literal"~~~
      }
    `);

    const diagnostics = await Tester.diagnose(source);
    expectDiagnostics(diagnostics, {
      code: "intersect-non-model",
      message: "Cannot intersect non-model types (including union types).",
      pos,
      end,
    });
  });
});

// https://github.com/microsoft/typespec/issues/2826
describe("ensure the target model is completely resolved before intersecting", () => {
  it("declared before", async () => {
    const { A } = await Tester.compile(t.code`
      model ${t.model("A")} { ...Alias }

      model B {
        b: A;
        prop: string;
      }

      alias Alias = B & {};

    `);
    expect(A.properties.has("b")).toBe(true);
    expect(A.properties.has("prop")).toBe(true);
  });

  it("declared after", async () => {
    const { A } = await Tester.compile(t.code`
      model B {
        b: A;
        prop: string;
      }

      alias Alias = B & {};

      model ${t.model("A")} { ...Alias }
    `);
    expect(A.properties.has("b")).toBe(true);
    expect(A.properties.has("prop")).toBe(true);
  });
});
