import { describe, expect, it } from "vitest";
import {
  executeConnectSecretRefresh,
  listNostrAccountIds,
  resolveDefaultNostrAccountId,
  resolveNostrAccount,
  resolveSecretValue,
} from "./types.js";

const TEST_PRIVATE_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

describe("listNostrAccountIds", () => {
  it("returns empty array when not configured", () => {
    const cfg = { channels: {} };
    expect(listNostrAccountIds(cfg)).toEqual([]);
  });

  it("returns empty array when nostr section exists but no privateKey", () => {
    const cfg = { channels: { nostr: { enabled: true } } };
    expect(listNostrAccountIds(cfg)).toEqual([]);
  });

  it("returns default when privateKey is configured", () => {
    const cfg = {
      channels: {
        nostr: { privateKey: TEST_PRIVATE_KEY },
      },
    };
    expect(listNostrAccountIds(cfg)).toEqual(["default"]);
  });
});

describe("resolveDefaultNostrAccountId", () => {
  it("returns default when configured", () => {
    const cfg = {
      channels: {
        nostr: { privateKey: TEST_PRIVATE_KEY },
      },
    };
    expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
  });

  it("returns default when not configured", () => {
    const cfg = { channels: {} };
    expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
  });
});

describe("resolveNostrAccount", () => {
  it("resolves configured account", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: TEST_PRIVATE_KEY,
          name: "Test Bot",
          relays: ["wss://test.relay"],
          dmPolicy: "pairing" as const,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.accountId).toBe("default");
    expect(account.name).toBe("Test Bot");
    expect(account.enabled).toBe(true);
    expect(account.configured).toBe(true);
    expect(account.privateKey).toBe(TEST_PRIVATE_KEY);
    expect(account.publicKey).toMatch(/^[0-9a-f]{64}$/);
    expect(account.relays).toEqual(["wss://test.relay"]);
  });

  it("resolves unconfigured account with defaults", () => {
    const cfg = { channels: {} };
    const account = resolveNostrAccount({ cfg });

    expect(account.accountId).toBe("default");
    expect(account.enabled).toBe(true);
    expect(account.configured).toBe(false);
    expect(account.privateKey).toBe("");
    expect(account.publicKey).toBe("");
    expect(account.relays).toContain("wss://relay.sharegap.net");
    expect(account.relays).toContain("wss://nos.lol");
  });

  it("handles disabled channel", () => {
    const cfg = {
      channels: {
        nostr: {
          enabled: false,
          privateKey: TEST_PRIVATE_KEY,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.enabled).toBe(false);
    expect(account.configured).toBe(true);
  });

  it("handles custom accountId parameter", () => {
    const cfg = {
      channels: {
        nostr: { privateKey: TEST_PRIVATE_KEY },
      },
    };
    const account = resolveNostrAccount({ cfg, accountId: "custom" });

    expect(account.accountId).toBe("custom");
  });

  it("handles allowFrom config", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: TEST_PRIVATE_KEY,
          allowFrom: ["npub1test", "0123456789abcdef"],
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.config.allowFrom).toEqual(["npub1test", "0123456789abcdef"]);
  });

  it("handles invalid private key gracefully", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: "invalid-key",
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.configured).toBe(true); // key is present
    expect(account.publicKey).toBe(""); // but can't derive pubkey
  });

  it("preserves all config options", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: TEST_PRIVATE_KEY,
          name: "Bot",
          enabled: true,
          relays: ["wss://relay1", "wss://relay2"],
          dmPolicy: "allowlist" as const,
          allowFrom: ["pubkey1", "pubkey2"],
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.config.privateKey).toBe(TEST_PRIVATE_KEY);
    expect(account.config.name).toBe("Bot");
    expect(account.config.enabled).toBe(true);
    expect(account.config.relays).toEqual(["wss://relay1", "wss://relay2"]);
    expect(account.config.dmPolicy).toBe("allowlist");
    expect(account.config.allowFrom).toEqual(["pubkey1", "pubkey2"]);
  });
});

describe("NIP-46 remote signing config", () => {
  const BUNKER_PUBKEY = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
  const BUNKER_URL = `bunker://${BUNKER_PUBKEY}?relay=wss://relay.nsecbunker.com`;
  const CLIENT_SECRET = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; // pragma: allowlist secret

  it("resolves NIP-46 account as configured when bunker URL and secret are present", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.nip46Enabled).toBe(true);
    expect(account.configured).toBe(true);
    expect(account.nip46BunkerUrl).toBe(BUNKER_URL);
    expect(account.nip46Secret).toBe(CLIENT_SECRET);
    expect(account.publicKey).toBe(BUNKER_PUBKEY);
  });

  it("resolves NIP-46 account as unconfigured when secret is missing", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.nip46Enabled).toBe(true);
    expect(account.configured).toBe(false);
  });

  it("resolves NIP-46 account as unconfigured when bunker URL is missing", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46Secret: CLIENT_SECRET,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.nip46Enabled).toBe(true);
    expect(account.configured).toBe(false);
  });

  it("lists NIP-46 account IDs when bunker URL is configured", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
        },
      },
    };
    expect(listNostrAccountIds(cfg)).toEqual(["default"]);
  });

  it("resolves NIP-46 secret from SecretRef env source", () => {
    const originalEnv = process.env.TEST_NIP46_SECRET;
    process.env.TEST_NIP46_SECRET = CLIENT_SECRET;
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
            nip46Secret: { source: "env", provider: "default", id: "TEST_NIP46_SECRET" },
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET);
      expect(account.configured).toBe(true);
    } finally {
      if (originalEnv === undefined) {
        delete process.env.TEST_NIP46_SECRET;
      } else {
        process.env.TEST_NIP46_SECRET = originalEnv;
      }
    }
  });

  it("falls back to NOSTR_NIP46_SECRET env var", () => {
    const originalEnv = process.env.NOSTR_NIP46_SECRET;
    process.env.NOSTR_NIP46_SECRET = CLIENT_SECRET;
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET);
      expect(account.configured).toBe(true);
    } finally {
      if (originalEnv === undefined) {
        delete process.env.NOSTR_NIP46_SECRET;
      } else {
        process.env.NOSTR_NIP46_SECRET = originalEnv;
      }
    }
  });

  it("does not resolve NIP-46 fields when nip46 is false", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: TEST_PRIVATE_KEY,
          nip46: false,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.nip46Enabled).toBe(false);
    expect(account.configured).toBe(true); // via privateKey
    expect(account.nip46Secret).toBeUndefined(); // not resolved when nip46 is off
  });

  it("resolves NIP-46 secret from SecretRef env source with 'name' field", () => {
    const originalEnv = process.env.TEST_NIP46_SECRET_NAME;
    process.env.TEST_NIP46_SECRET_NAME = CLIENT_SECRET;
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
            nip46Secret: { source: "env", name: "TEST_NIP46_SECRET_NAME" },
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET);
      expect(account.configured).toBe(true);
    } finally {
      if (originalEnv === undefined) {
        delete process.env.TEST_NIP46_SECRET_NAME;
      } else {
        process.env.TEST_NIP46_SECRET_NAME = originalEnv;
      }
    }
  });

  it("resolves NIP-46 secret from file source", () => {
    const fs = require("node:fs");
    const path = require("node:path");
    const os = require("node:os");
    const tmpFile = path.join(os.tmpdir(), `nip46-test-secret-${Date.now()}.txt`);
    fs.writeFileSync(tmpFile, `  ${CLIENT_SECRET}  \n`);
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
            nip46Secret: { source: "file", path: tmpFile },
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET);
      expect(account.configured).toBe(true);
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });

  it("falls back to env var when file source is unreadable", () => {
    const originalEnv = process.env.NOSTR_NIP46_SECRET;
    process.env.NOSTR_NIP46_SECRET = CLIENT_SECRET;
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
            nip46Secret: { source: "file", path: "/nonexistent/path/secret.txt" },
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET); // fallback to env
    } finally {
      if (originalEnv === undefined) {
        delete process.env.NOSTR_NIP46_SECRET;
      } else {
        process.env.NOSTR_NIP46_SECRET = originalEnv;
      }
    }
  });

  it("resolves NIP-46 secret from exec source", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: { source: "exec", command: `echo ${CLIENT_SECRET}` },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46Secret).toBe(CLIENT_SECRET);
    expect(account.configured).toBe(true);
  });

  it("falls back to env var when exec source fails", () => {
    const originalEnv = process.env.NOSTR_NIP46_SECRET;
    process.env.NOSTR_NIP46_SECRET = CLIENT_SECRET;
    try {
      const cfg = {
        channels: {
          nostr: {
            nip46: true,
            nip46BunkerUrl: BUNKER_URL,
            nip46Secret: { source: "exec", command: "false" },
          },
        },
      };
      const account = resolveNostrAccount({ cfg });
      expect(account.nip46Secret).toBe(CLIENT_SECRET); // fallback to env
    } finally {
      if (originalEnv === undefined) {
        delete process.env.NOSTR_NIP46_SECRET;
      } else {
        process.env.NOSTR_NIP46_SECRET = originalEnv;
      }
    }
  });

  it("preserves NIP-46 config options", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46SignerRelays: ["wss://relay.nsecbunker.com"],
          nip46ConnectionTimeoutMs: 30000,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });

    expect(account.config.nip46).toBe(true);
    expect(account.config.nip46BunkerUrl).toBe(BUNKER_URL);
    expect(account.config.nip46SignerRelays).toEqual(["wss://relay.nsecbunker.com"]);
    expect(account.config.nip46ConnectionTimeoutMs).toBe(30000);
    expect(account.nip46ConnectionTimeoutMs).toBe(30000);
  });
});

describe("resolveSecretValue", () => {
  it("returns undefined for undefined/null input", () => {
    expect(resolveSecretValue(undefined)).toBeUndefined();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(resolveSecretValue(null as any)).toBeUndefined();
  });

  it("returns trimmed string for plain string input", () => {
    expect(resolveSecretValue("  mysecret  ")).toBe("mysecret");
    expect(resolveSecretValue("")).toBeUndefined();
    expect(resolveSecretValue("   ")).toBeUndefined();
  });

  it("resolves from env source", () => {
    const original = process.env.TEST_RESOLVE_SECRET;
    process.env.TEST_RESOLVE_SECRET = "env-secret-value";
    try {
      expect(resolveSecretValue({ source: "env", id: "TEST_RESOLVE_SECRET" })).toBe("env-secret-value");
    } finally {
      if (original === undefined) delete process.env.TEST_RESOLVE_SECRET;
      else process.env.TEST_RESOLVE_SECRET = original;
    }
  });

  it("resolves from env source using 'name' field", () => {
    const original = process.env.TEST_RESOLVE_SECRET_NAME;
    process.env.TEST_RESOLVE_SECRET_NAME = "env-name-value";
    try {
      expect(resolveSecretValue({ source: "env", name: "TEST_RESOLVE_SECRET_NAME" })).toBe("env-name-value");
    } finally {
      if (original === undefined) delete process.env.TEST_RESOLVE_SECRET_NAME;
      else process.env.TEST_RESOLVE_SECRET_NAME = original;
    }
  });

  it("resolves from file source", () => {
    const fs = require("node:fs");
    const path = require("node:path");
    const os = require("node:os");
    const tmpFile = path.join(os.tmpdir(), `resolve-secret-test-${Date.now()}.txt`);
    fs.writeFileSync(tmpFile, "  file-secret-value  \n");
    try {
      expect(resolveSecretValue({ source: "file", path: tmpFile })).toBe("file-secret-value");
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });

  it("returns undefined for unreadable file", () => {
    expect(resolveSecretValue({ source: "file", path: "/nonexistent/path.txt" })).toBeUndefined();
  });

  it("resolves from exec source with string command", () => {
    expect(resolveSecretValue({ source: "exec", command: "echo exec-secret-value" })).toBe("exec-secret-value");
  });

  it("resolves from exec source with array command", () => {
    expect(resolveSecretValue({ source: "exec", command: ["echo", "exec-array-value"] })).toBe("exec-array-value");
  });

  it("returns undefined for failing exec command", () => {
    expect(resolveSecretValue({ source: "exec", command: "false" })).toBeUndefined();
  });
});

describe("executeConnectSecretRefresh", () => {
  it("returns false for undefined spec", () => {
    expect(executeConnectSecretRefresh(undefined)).toBe(false);
  });

  it("returns false for non-exec source", () => {
    expect(executeConnectSecretRefresh({ source: "env", id: "FOO" } as any)).toBe(false);
    expect(executeConnectSecretRefresh({ source: "file", path: "/tmp/x" } as any)).toBe(false);
  });

  it("returns true when exec command succeeds (string)", () => {
    expect(executeConnectSecretRefresh({ source: "exec", command: "true" })).toBe(true);
  });

  it("returns true when exec command succeeds (array)", () => {
    expect(executeConnectSecretRefresh({ source: "exec", command: ["echo", "refreshed"] })).toBe(true);
  });

  it("returns false when exec command fails", () => {
    expect(executeConnectSecretRefresh({ source: "exec", command: "false" })).toBe(false);
  });
});

describe("NIP-46 connect secret config resolution", () => {
  const BUNKER_PUBKEY = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
  const BUNKER_URL = `bunker://${BUNKER_PUBKEY}?relay=wss://relay.nsecbunker.com`;
  const CLIENT_SECRET = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; // pragma: allowlist secret

  it("resolves nip46ConnectSecret as plain string", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46ConnectSecret: "my-connect-secret",
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ConnectSecret).toBe("my-connect-secret");
  });

  it("resolves nip46ConnectSecret as SecretRef", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46ConnectSecret: { source: "exec", command: ["echo", "dynamic-secret"] },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ConnectSecret).toEqual({ source: "exec", command: ["echo", "dynamic-secret"] });
  });

  it("resolves nip46ConnectSecretRefresh as SecretRef", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46ConnectSecretRefresh: { source: "exec", command: ["/usr/local/bin/helper", "refresh"] },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ConnectSecretRefresh).toEqual({
      source: "exec",
      command: ["/usr/local/bin/helper", "refresh"],
    });
  });

  it("provides default reconnect policy", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ReconnectPolicy).toEqual({
      refreshOnStart: false,
      refreshOnMismatch: true,
      maxRefreshAttempts: 1,
    });
  });

  it("merges custom reconnect policy with defaults", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46ReconnectPolicy: {
            refreshOnStart: true,
            maxRefreshAttempts: 3,
          },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ReconnectPolicy).toEqual({
      refreshOnStart: true,
      refreshOnMismatch: true, // default preserved
      maxRefreshAttempts: 3,
    });
  });

  it("does not resolve connect secret fields when nip46 is false", () => {
    const cfg = {
      channels: {
        nostr: {
          privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
          nip46: false,
          nip46ConnectSecret: "should-not-resolve",
          nip46ConnectSecretRefresh: { source: "exec", command: "true" },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.nip46ConnectSecret).toBeUndefined();
    expect(account.nip46ConnectSecretRefresh).toBeUndefined();
  });

  it("preserves connect secret config in config passthrough", () => {
    const cfg = {
      channels: {
        nostr: {
          nip46: true,
          nip46BunkerUrl: BUNKER_URL,
          nip46Secret: CLIENT_SECRET,
          nip46ConnectSecret: "my-secret",
          nip46ConnectSecretRefresh: { source: "exec", command: "refresh-cmd" },
          nip46ReconnectPolicy: { refreshOnStart: true },
        },
      },
    };
    const account = resolveNostrAccount({ cfg });
    expect(account.config.nip46ConnectSecret).toBe("my-secret");
    expect(account.config.nip46ConnectSecretRefresh).toEqual({ source: "exec", command: "refresh-cmd" });
    expect(account.config.nip46ReconnectPolicy).toEqual({ refreshOnStart: true });
  });
});
