import { describe, it, expect, ts, expandUtilityType, assertDefined, createTestProgram, findTypeAliasReference, stubConvertType, } from "./helpers.js";
describe("Utility Type Expansion Safety", () => {
    describe("Index signatures block expansion", () => {
        it("should return null for Partial<T> when T has string index signature", () => {
            const source = `
        interface WithStringIndex {
          [key: string]: number;
          name: string;
        }
        type PartialWithIndex = Partial<WithStringIndex>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialWithIndex");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            // Should return null because expansion would lose the index signature
            expect(result).to.equal(null);
        });
        it("should return null for Readonly<T> when T has number index signature", () => {
            const source = `
        interface WithNumberIndex {
          [key: number]: string;
          length: number;
        }
        type ReadonlyWithIndex = Readonly<WithNumberIndex>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "ReadonlyWithIndex");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Readonly", binding, stubConvertType);
            // Should return null because expansion would lose the index signature
            expect(result).to.equal(null);
        });
        it("should expand normally when T has no index signatures", () => {
            const source = `
        interface Person {
          name: string;
          age: number;
        }
        type PartialPerson = Partial<Person>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialPerson");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            // Should expand successfully
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            expect(result?.members).to.have.length(2);
        });
    });
    describe("Symbol/computed keys block expansion", () => {
        it("should return null when T has symbol keys", () => {
            // Note: Symbol keys in TypeScript are represented with __@ prefix internally
            // This test validates that the expansion correctly identifies and rejects them
            const source = `
        const sym = Symbol("test");
        interface WithSymbol {
          [sym]: string;
          name: string;
        }
        type PartialWithSymbol = Partial<WithSymbol>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialWithSymbol");
            // This may or may not find the type ref depending on how TS handles it
            if (typeRef) {
                const result = expandUtilityType(typeRef, "Partial", binding, stubConvertType);
                // If expansion proceeds, it should return null due to symbol key
                // (symbol keys start with __@ internally)
                // Note: The actual behavior depends on whether TS resolves the symbol key
                // Either result is null (rejected) or expanded (symbol was ignored)
                expect(result === null || result.kind === "objectType").to.equal(true);
            }
            // Test passes if we get here - the key insight is the code handles this case
        });
    });
    describe("Explicit undefined preservation", () => {
        it("should preserve explicit undefined in optional property type", () => {
            const source = `
        interface WithExplicitUndefined {
          x?: string | undefined;
        }
        type PartialWithUndefined = Partial<WithExplicitUndefined>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialWithUndefined");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // The property should preserve the union with undefined
            const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
            expect(xProp).not.to.equal(undefined);
            // The type should be a union containing undefined
            if (xProp && xProp.kind === "propertySignature") {
                // With explicit undefined, the type should include undefined in the union
                // The exact representation depends on whether we stripped synthetic undefined
                // The key is that we DON'T strip it when explicit undefined was declared
                expect(xProp.type.kind).to.equal("unionType");
            }
        });
        it("should strip synthetic undefined from optional property without explicit undefined", () => {
            const source = `
        interface WithSyntheticUndefined {
          x?: string;
        }
        type PartialWithSynthetic = Partial<WithSyntheticUndefined>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialWithSynthetic");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // The property type should be string (not string | undefined)
            // because we strip synthetic undefined
            const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
            expect(xProp).not.to.equal(undefined);
            if (xProp && xProp.kind === "propertySignature") {
                // Synthetic undefined should be stripped, leaving just string
                expect(xProp.type.kind).to.equal("primitiveType");
                if (xProp.type.kind === "primitiveType") {
                    expect(xProp.type.name).to.equal("string");
                }
            }
        });
        it("should preserve explicit undefined in required property", () => {
            const source = `
        interface WithRequiredUndefined {
          x: string | undefined;
        }
        type PartialRequired = Partial<WithRequiredUndefined>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialRequired");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            expect(result).not.to.equal(null);
            const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
            expect(xProp).not.to.equal(undefined);
            // Required property with explicit undefined should keep the union
            if (xProp && xProp.kind === "propertySignature") {
                expect(xProp.type.kind).to.equal("unionType");
            }
        });
    });
    describe("Readonly preservation in nested utility types", () => {
        it("should preserve readonly in Partial<Readonly<T>>", () => {
            const source = `
        interface Person {
          name: string;
          age: number;
        }
        type PartialReadonly = Partial<Readonly<Person>>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialReadonly");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // All properties should be readonly (from inner Readonly<T>)
            const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
            expect(nameProp).not.to.equal(undefined);
            if (nameProp && nameProp.kind === "propertySignature") {
                expect(nameProp.isReadonly).to.equal(true);
                expect(nameProp.isOptional).to.equal(true);
            }
        });
        it("should preserve readonly and optional in Readonly<Partial<T>>", () => {
            const source = `
        interface Person {
          name: string;
          age: number;
        }
        type ReadonlyPartial = Readonly<Partial<Person>>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "ReadonlyPartial");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Readonly", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // All properties should be both readonly (from Readonly<T>) and optional (from Partial<T>)
            const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
            expect(nameProp).not.to.equal(undefined);
            if (nameProp && nameProp.kind === "propertySignature") {
                expect(nameProp.isReadonly).to.equal(true);
                expect(nameProp.isOptional).to.equal(true);
            }
        });
    });
    describe("Method signatures in utility types", () => {
        it("should expand interface with method as methodSignature", () => {
            const source = `
        interface WithMethod {
          name: string;
          greet(greeting: string): string;
        }
        type PartialWithMethod = Partial<WithMethod>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "PartialWithMethod");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // Should have both property and method
            const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
            const greetMethod = result?.members.find((m) => m.kind === "methodSignature" && m.name === "greet");
            expect(nameProp).not.to.equal(undefined);
            expect(greetMethod).not.to.equal(undefined);
            // Method should have parameters
            if (greetMethod && greetMethod.kind === "methodSignature") {
                expect(greetMethod.parameters).to.have.length(1);
            }
        });
        it("should ignore internal marker members during utility expansion", () => {
            const source = `
        interface Probe {
          readonly "__tsonic_binding_alias_Acme.Core.Probe"?: never;
          readonly __tsonic_type_Acme_Core_Probe?: never;
          readonly __tsonic_iface_Acme_Core_Probe?: never;
          value?: number;
          greet(name: string): string;
        }
        type RequiredProbe = Required<Probe>;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "RequiredProbe");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Required", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            const memberNames = result?.members.map((member) => member.name).sort() ?? [];
            expect(memberNames).to.deep.equal(["greet", "value"]);
        });
    });
    describe("Pick and Omit with multiple keys", () => {
        it("should expand Pick with multiple keys", () => {
            const source = `
        interface Person {
          name: string;
          age: number;
          email: string;
          phone: string;
        }
        type ContactInfo = Pick<Person, "name" | "email" | "phone">;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "ContactInfo");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Pick", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // Should only have name, email, phone (not age)
            expect(result?.members).to.have.length(3);
            const propNames = result?.members
                .filter((m) => m.kind === "propertySignature")
                .map((m) => m.name);
            expect(propNames).to.include("name");
            expect(propNames).to.include("email");
            expect(propNames).to.include("phone");
            expect(propNames).not.to.include("age");
        });
        it("should expand Omit with multiple keys", () => {
            const source = `
        interface Person {
          name: string;
          age: number;
          email: string;
          phone: string;
        }
        type MinimalPerson = Omit<Person, "email" | "phone">;
      `;
            const { binding, sourceFile } = createTestProgram(source);
            const typeRef = findTypeAliasReference(sourceFile, "MinimalPerson");
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Omit", binding, stubConvertType);
            expect(result).not.to.equal(null);
            expect(result?.kind).to.equal("objectType");
            // Should only have name, age (not email, phone)
            expect(result?.members).to.have.length(2);
            const propNames = result?.members
                .filter((m) => m.kind === "propertySignature")
                .map((m) => m.name);
            expect(propNames).to.include("name");
            expect(propNames).to.include("age");
            expect(propNames).not.to.include("email");
            expect(propNames).not.to.include("phone");
        });
    });
    describe("Type parameter detection", () => {
        it("should return null for Partial<T> where T is a type parameter", () => {
            const source = `
        function process<T>(data: Partial<T>): void {}
      `;
            const { binding, sourceFile } = createTestProgram(source);
            // Find the Partial<T> type reference in the function parameter
            let typeRef = null;
            const visitor = (node) => {
                if (ts.isTypeReferenceNode(node) &&
                    ts.isIdentifier(node.typeName) &&
                    node.typeName.text === "Partial") {
                    typeRef = node;
                }
                ts.forEachChild(node, visitor);
            };
            ts.forEachChild(sourceFile, visitor);
            const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", binding, stubConvertType);
            // Should return null because T is a type parameter - can't expand at compile time
            expect(result).to.equal(null);
        });
    });
});
//# sourceMappingURL=expansion-safety.test.js.map