import {
  createAccountTypeFromScriptType,
  determineAccountTypeFromWitnessUtxo,
  createAccountTypeFromAddressFormat,
  determineAccountTypeFromPurpose,
  determineAccountType,
} from "../../src/signPsbt/accountTypeResolver";
import { PsbtV2, SCRIPT_CONSTANTS, detectScriptType } from "@ledgerhq/psbtv2";

const masterFp = Buffer.from([1, 2, 3, 4]);

function makeP2wpkhScriptPubKey(): Buffer {
  const buf = Buffer.alloc(SCRIPT_CONSTANTS.P2WPKH.LENGTH);
  buf[0] = SCRIPT_CONSTANTS.P2WPKH.PREFIX[0];
  buf[1] = SCRIPT_CONSTANTS.P2WPKH.PREFIX[1];
  buf.fill(1, 2);
  return buf;
}

function makeP2trScriptPubKey(): Buffer {
  const buf = Buffer.alloc(SCRIPT_CONSTANTS.P2TR.LENGTH);
  buf[0] = SCRIPT_CONSTANTS.P2TR.PREFIX[0];
  buf[1] = SCRIPT_CONSTANTS.P2TR.PREFIX[1];
  buf.fill(2, 2);
  return buf;
}

function makeP2shScriptPubKey(): Buffer {
  const buf = Buffer.alloc(SCRIPT_CONSTANTS.P2SH.LENGTH);
  buf[0] = SCRIPT_CONSTANTS.P2SH.PREFIX[0];
  buf[1] = SCRIPT_CONSTANTS.P2SH.PREFIX[1];
  buf.fill(3, 2, buf.length - 1);
  buf[buf.length - 1] = SCRIPT_CONSTANTS.P2SH.SUFFIX[0];
  return buf;
}

function makeP2pkhScriptPubKey(): Buffer {
  const buf = Buffer.alloc(SCRIPT_CONSTANTS.P2PKH.LENGTH);
  buf[0] = SCRIPT_CONSTANTS.P2PKH.PREFIX[0];
  buf[1] = SCRIPT_CONSTANTS.P2PKH.PREFIX[1];
  buf[2] = SCRIPT_CONSTANTS.P2PKH.PREFIX[2];
  buf.fill(4, 3, 23);
  buf[23] = SCRIPT_CONSTANTS.P2PKH.SUFFIX[0];
  buf[24] = SCRIPT_CONSTANTS.P2PKH.SUFFIX[1];
  return buf;
}

describe("accountTypeResolver", () => {
  const psbt = {} as unknown as PsbtV2;

  describe("detectScriptType", () => {
    it("returns p2wpkh for valid P2WPKH scriptPubKey", () => {
      expect(detectScriptType(makeP2wpkhScriptPubKey())).toBe("p2wpkh");
    });

    it("returns p2tr for valid P2TR scriptPubKey", () => {
      expect(detectScriptType(makeP2trScriptPubKey())).toBe("p2tr");
    });

    it("returns p2sh for valid P2SH scriptPubKey", () => {
      expect(detectScriptType(makeP2shScriptPubKey())).toBe("p2sh");
    });

    it("returns p2pkh for valid P2PKH scriptPubKey", () => {
      expect(detectScriptType(makeP2pkhScriptPubKey())).toBe("p2pkh");
    });

    it("returns undefined for unknown script", () => {
      expect(detectScriptType(Buffer.from([0x6a, 0x00]))).toBeUndefined();
    });
  });

  describe("createAccountTypeFromScriptType", () => {
    it("returns account type with wpkh template for p2wpkh", () => {
      const accountType = createAccountTypeFromScriptType("p2wpkh", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("returns account type with tr template for p2tr", () => {
      const accountType = createAccountTypeFromScriptType("p2tr", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("tr(@0/**)");
    });

    it("returns account type with sh(wpkh) template for p2sh and p2sh-p2wpkh", () => {
      expect(createAccountTypeFromScriptType("p2sh", psbt, masterFp).getDescriptorTemplate()).toBe(
        "sh(wpkh(@0/**))",
      );
      expect(
        createAccountTypeFromScriptType("p2sh-p2wpkh", psbt, masterFp).getDescriptorTemplate(),
      ).toBe("sh(wpkh(@0/**))");
    });

    it("returns account type with pkh template for p2pkh", () => {
      const accountType = createAccountTypeFromScriptType("p2pkh", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("pkh(@0/**)");
    });
  });

  describe("determineAccountTypeFromWitnessUtxo", () => {
    it("returns null when input has no witness UTXO", () => {
      const psbtNoWitness = {
        getInputWitnessUtxo: () => undefined,
      } as unknown as PsbtV2;
      expect(determineAccountTypeFromWitnessUtxo(psbtNoWitness, 0, masterFp)).toBeNull();
    });

    it("returns account type when witness UTXO has known script type", () => {
      const psbtWithWitness = {
        getInputWitnessUtxo: () => ({ scriptPubKey: makeP2wpkhScriptPubKey() }),
      } as unknown as PsbtV2;
      const result = determineAccountTypeFromWitnessUtxo(psbtWithWitness, 0, masterFp);
      expect(result).not.toBeNull();
      expect(result!.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("throws when witness UTXO has unsupported script type", () => {
      const psbtWithUnknown = {
        getInputWitnessUtxo: () => ({ scriptPubKey: Buffer.from([0x00, 0x01]) }),
      } as unknown as PsbtV2;
      expect(() => determineAccountTypeFromWitnessUtxo(psbtWithUnknown, 0, masterFp)).toThrow(
        /Unsupported script type/,
      );
    });
  });

  describe("createAccountTypeFromAddressFormat", () => {
    it("returns p2pkh for legacy format", () => {
      const accountType = createAccountTypeFromAddressFormat("legacy", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("pkh(@0/**)");
    });

    it("returns p2wpkhWrapped for p2sh format", () => {
      const accountType = createAccountTypeFromAddressFormat("p2sh", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("sh(wpkh(@0/**))");
    });

    it("returns p2wpkh for bech32 format", () => {
      const accountType = createAccountTypeFromAddressFormat("bech32", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("returns p2tr for bech32m format", () => {
      const accountType = createAccountTypeFromAddressFormat("bech32m", psbt, masterFp);
      expect(accountType.getDescriptorTemplate()).toBe("tr(@0/**)");
    });

    it("throws for unsupported address format", () => {
      expect(() => createAccountTypeFromAddressFormat("unknown" as any, psbt, masterFp)).toThrow(
        /Unsupported address format/,
      );
    });
  });

  describe("determineAccountTypeFromPurpose", () => {
    it("returns null when account path is empty", () => {
      expect(determineAccountTypeFromPurpose([], psbt, masterFp)).toBeNull();
    });

    it("returns p2pkh for purpose 44'", () => {
      const result = determineAccountTypeFromPurpose(
        [0x8000002c, 0x80000000, 0x80000000],
        psbt,
        masterFp,
      );
      expect(result).not.toBeNull();
      expect(result!.getDescriptorTemplate()).toBe("pkh(@0/**)");
    });

    it("returns p2wpkhWrapped for purpose 49'", () => {
      const result = determineAccountTypeFromPurpose(
        [0x80000031, 0x80000000, 0x80000000],
        psbt,
        masterFp,
      );
      expect(result).not.toBeNull();
      expect(result!.getDescriptorTemplate()).toBe("sh(wpkh(@0/**))");
    });

    it("returns p2wpkh for purpose 84'", () => {
      const result = determineAccountTypeFromPurpose(
        [0x80000054, 0x80000000, 0x80000000],
        psbt,
        masterFp,
      );
      expect(result).not.toBeNull();
      expect(result!.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("returns p2tr for purpose 86'", () => {
      const result = determineAccountTypeFromPurpose(
        [0x80000056, 0x80000000, 0x80000000],
        psbt,
        masterFp,
      );
      expect(result).not.toBeNull();
      expect(result!.getDescriptorTemplate()).toBe("tr(@0/**)");
    });

    it("returns null for unknown purpose", () => {
      expect(determineAccountTypeFromPurpose([0x80000000, 0x80000000], psbt, masterFp)).toBeNull();
    });
  });

  describe("determineAccountType", () => {
    it("uses detectedScriptType when provided", () => {
      const result = determineAccountType(psbt, 0, masterFp, "p2tr", [], undefined);
      expect(result.getDescriptorTemplate()).toBe("tr(@0/**)");
    });

    it("falls back to witness UTXO when no detectedScriptType", () => {
      const psbtWithWitness = {
        getInputWitnessUtxo: () => ({ scriptPubKey: makeP2wpkhScriptPubKey() }),
        getInputRedeemScript: () => undefined,
      } as unknown as PsbtV2;
      const result = determineAccountType(psbtWithWitness, 0, masterFp, undefined, [], undefined);
      expect(result.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("falls back to redeem script (p2sh-p2wpkh) when no witness UTXO", () => {
      const psbtWithRedeem = {
        getInputWitnessUtxo: () => undefined,
        getInputRedeemScript: () => Buffer.alloc(1),
      } as unknown as PsbtV2;
      const result = determineAccountType(psbtWithRedeem, 0, masterFp, undefined, [], undefined);
      expect(result.getDescriptorTemplate()).toBe("sh(wpkh(@0/**))");
    });

    it("falls back to addressFormat when no witness or redeem script", () => {
      const psbtEmpty = {
        getInputWitnessUtxo: () => undefined,
        getInputRedeemScript: () => undefined,
      } as unknown as PsbtV2;
      const result = determineAccountType(psbtEmpty, 0, masterFp, undefined, [], "bech32m");
      expect(result.getDescriptorTemplate()).toBe("tr(@0/**)");
    });

    it("falls back to purpose from account path when no addressFormat", () => {
      const psbtEmpty = {
        getInputWitnessUtxo: () => undefined,
        getInputRedeemScript: () => undefined,
      } as unknown as PsbtV2;
      const result = determineAccountType(
        psbtEmpty,
        0,
        masterFp,
        undefined,
        [0x80000054, 0x80000000, 0x80000000],
        undefined,
      );
      expect(result.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });

    it("defaults to p2wpkh when no other source available", () => {
      const psbtEmpty = {
        getInputWitnessUtxo: () => undefined,
        getInputRedeemScript: () => undefined,
      } as unknown as PsbtV2;
      const result = determineAccountType(
        psbtEmpty,
        0,
        masterFp,
        undefined,
        [0x80000000],
        undefined,
      );
      expect(result.getDescriptorTemplate()).toBe("wpkh(@0/**)");
    });
  });
});
