import { describe, expect, it, vi } from "vitest";
import type { LookupFn } from "../infra/net/ssrf.js";
import {
  assertHttpUrlTargetsPrivateNetwork,
  buildHostnameAllowlistPolicyFromSuffixAllowlist,
  hasLegacyFlatAllowPrivateNetworkAlias,
  isPrivateNetworkOptInEnabled,
  isHttpsUrlAllowedByHostnameSuffixAllowlist,
  migrateLegacyFlatAllowPrivateNetworkAlias,
  normalizeHostnameSuffixAllowlist,
  ssrfPolicyFromDangerouslyAllowPrivateNetwork,
  ssrfPolicyFromAllowPrivateNetwork,
  ssrfPolicyFromPrivateNetworkOptIn,
} from "./ssrf-policy.js";

function createLookupFn(addresses: Array<{ address: string; family: number }>): LookupFn {
  return vi.fn(async (_hostname: string, options?: unknown) => {
    if (typeof options === "number" || !options || !(options as { all?: boolean }).all) {
      return addresses[0];
    }
    return addresses;
  }) as unknown as LookupFn;
}

describe("ssrfPolicyFromDangerouslyAllowPrivateNetwork", () => {
  it.each([
    {
      name: "returns undefined for missing input",
      input: undefined,
      expected: undefined,
    },
    {
      name: "returns undefined when private-network access is disabled",
      input: false,
      expected: undefined,
    },
    {
      name: "returns an explicit allow-private-network policy when enabled",
      input: true,
      expected: { allowPrivateNetwork: true },
    },
  ])("$name", ({ input, expected }) => {
    expect(ssrfPolicyFromDangerouslyAllowPrivateNetwork(input)).toEqual(expected);
  });
});

describe("ssrfPolicyFromAllowPrivateNetwork", () => {
  it.each([
    {
      name: "returns undefined for missing input",
      input: undefined,
      expected: undefined,
    },
    {
      name: "returns undefined when private-network access is disabled",
      input: false,
      expected: undefined,
    },
    {
      name: "returns an explicit allow-private-network policy when enabled",
      input: true,
      expected: { allowPrivateNetwork: true },
    },
  ])("$name", ({ input, expected }) => {
    expect(ssrfPolicyFromAllowPrivateNetwork(input)).toEqual(expected);
  });
});

describe("isPrivateNetworkOptInEnabled", () => {
  it.each([
    {
      name: "returns false for missing input",
      input: undefined,
      expected: false,
    },
    {
      name: "returns false for explicit false",
      input: false,
      expected: false,
    },
    {
      name: "returns true for explicit boolean true",
      input: true,
      expected: true,
    },
    {
      name: "returns true for flat allowPrivateNetwork config",
      input: { allowPrivateNetwork: true },
      expected: true,
    },
    {
      name: "returns true for flat dangerous opt-in config",
      input: { dangerouslyAllowPrivateNetwork: true },
      expected: true,
    },
    {
      name: "returns true for nested network dangerous opt-in config",
      input: { network: { dangerouslyAllowPrivateNetwork: true } },
      expected: true,
    },
    {
      name: "returns false for nested false values",
      input: { network: { dangerouslyAllowPrivateNetwork: false } },
      expected: false,
    },
  ])("$name", ({ input, expected }) => {
    expect(isPrivateNetworkOptInEnabled(input)).toBe(expected);
  });
});

describe("ssrfPolicyFromPrivateNetworkOptIn", () => {
  it.each([
    {
      name: "returns undefined for unset input",
      input: undefined,
      expected: undefined,
    },
    {
      name: "returns undefined for explicit false input",
      input: { allowPrivateNetwork: false },
      expected: undefined,
    },
    {
      name: "returns the compat policy for nested dangerous input",
      input: { network: { dangerouslyAllowPrivateNetwork: true } },
      expected: { allowPrivateNetwork: true },
    },
  ])("$name", ({ input, expected }) => {
    expect(ssrfPolicyFromPrivateNetworkOptIn(input)).toEqual(expected);
  });
});

describe("legacy private-network alias helpers", () => {
  it("detects the flat allowPrivateNetwork alias", () => {
    expect(hasLegacyFlatAllowPrivateNetworkAlias({ allowPrivateNetwork: true })).toBe(true);
    expect(hasLegacyFlatAllowPrivateNetworkAlias({ network: {} })).toBe(false);
  });

  it("migrates the flat alias into network.dangerouslyAllowPrivateNetwork", () => {
    const changes: string[] = [];
    const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({
      entry: { allowPrivateNetwork: true },
      pathPrefix: "channels.matrix",
      changes,
    });

    expect(migrated.entry).toEqual({
      network: {
        dangerouslyAllowPrivateNetwork: true,
      },
    });
    expect(changes).toEqual([
      "Moved channels.matrix.allowPrivateNetwork → channels.matrix.network.dangerouslyAllowPrivateNetwork (true).",
    ]);
  });

  it("prefers the canonical network key when both old and new keys are present", () => {
    const changes: string[] = [];
    const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({
      entry: {
        allowPrivateNetwork: true,
        network: {
          dangerouslyAllowPrivateNetwork: false,
        },
      },
      pathPrefix: "channels.matrix.accounts.default",
      changes,
    });

    expect(migrated.entry).toEqual({
      network: {
        dangerouslyAllowPrivateNetwork: false,
      },
    });
    expect(changes[0]).toContain("(false)");
  });

  it("keeps an explicit canonical true when the legacy key is false", () => {
    const changes: string[] = [];
    const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({
      entry: {
        allowPrivateNetwork: false,
        network: {
          dangerouslyAllowPrivateNetwork: true,
        },
      },
      pathPrefix: "channels.matrix.accounts.default",
      changes,
    });

    expect(migrated.entry).toEqual({
      network: {
        dangerouslyAllowPrivateNetwork: true,
      },
    });
    expect(changes[0]).toContain("(true)");
  });
});

describe("assertHttpUrlTargetsPrivateNetwork", () => {
  it.each([
    {
      name: "allows https targets without private-network checks",
      url: "https://matrix.example.org",
      policy: {
        dangerouslyAllowPrivateNetwork: false,
      },
      outcome: "resolve",
    },
    {
      name: "allows internal DNS names only when they resolve exclusively to private IPs",
      url: "http://matrix-synapse:8008",
      policy: {
        dangerouslyAllowPrivateNetwork: true,
        lookupFn: createLookupFn([{ address: "10.0.0.5", family: 4 }]),
      },
      outcome: "resolve",
    },
    {
      name: "rejects cleartext public hosts even when private-network access is enabled",
      url: "http://matrix.example.org:8008",
      policy: {
        dangerouslyAllowPrivateNetwork: true,
        lookupFn: createLookupFn([{ address: "93.184.216.34", family: 4 }]),
        errorMessage:
          "Matrix homeserver must use https:// unless it targets a private or loopback host",
      },
      outcome: "reject",
      expectedError:
        "Matrix homeserver must use https:// unless it targets a private or loopback host",
    },
  ])("$name", async ({ url, policy, outcome, expectedError }) => {
    const result = assertHttpUrlTargetsPrivateNetwork(url, policy);
    if (outcome === "reject") {
      await expect(result).rejects.toThrow(expectedError);
      return;
    }
    await expect(result).resolves.toBeUndefined();
  });

  it("prefers the canonical flag when both canonical and legacy flags are present", async () => {
    await expect(
      assertHttpUrlTargetsPrivateNetwork("http://matrix-synapse:8008", {
        dangerouslyAllowPrivateNetwork: false,
        allowPrivateNetwork: true,
        lookupFn: createLookupFn([{ address: "10.0.0.5", family: 4 }]),
      }),
    ).rejects.toThrow("HTTP URL must target a trusted private/internal host");
  });
});

describe("normalizeHostnameSuffixAllowlist", () => {
  it.each([
    {
      name: "uses defaults when input is missing",
      input: undefined,
      defaults: ["GRAPH.MICROSOFT.COM"],
      expected: ["graph.microsoft.com"],
    },
    {
      name: "normalizes wildcard prefixes and deduplicates",
      input: ["*.TrafficManager.NET", ".trafficmanager.net.", " * ", "x"],
      defaults: undefined,
      expected: ["*"],
    },
  ])("$name", ({ input, defaults, expected }) => {
    expect(normalizeHostnameSuffixAllowlist(input, defaults)).toEqual(expected);
  });
});

describe("isHttpsUrlAllowedByHostnameSuffixAllowlist", () => {
  it.each([
    {
      name: "requires https",
      url: "http://a.example.com/x",
      allowlist: ["example.com"],
      expected: false,
    },
    {
      name: "supports exact match",
      url: "https://example.com/x",
      allowlist: ["example.com"],
      expected: true,
    },
    {
      name: "supports suffix match",
      url: "https://a.example.com/x",
      allowlist: ["example.com"],
      expected: true,
    },
    {
      name: "rejects non-matching hosts",
      url: "https://evil.com/x",
      allowlist: ["example.com"],
      expected: false,
    },
    {
      name: "supports wildcard allowlist",
      url: "https://evil.com/x",
      allowlist: ["*"],
      expected: true,
    },
  ])("$name", ({ url, allowlist, expected }) => {
    expect(isHttpsUrlAllowedByHostnameSuffixAllowlist(url, allowlist)).toBe(expected);
  });
});

describe("buildHostnameAllowlistPolicyFromSuffixAllowlist", () => {
  it.each([
    {
      name: "returns undefined when allowHosts is empty",
      input: undefined,
      expected: undefined,
    },
    {
      name: "returns undefined for an explicit empty list",
      input: [],
      expected: undefined,
    },
    {
      name: "returns undefined when wildcard host is present",
      input: ["*"],
      expected: undefined,
    },
    {
      name: "returns undefined when wildcard is mixed with concrete hosts",
      input: ["example.com", "*"],
      expected: undefined,
    },
    {
      name: "expands a suffix entry to exact + wildcard hostname allowlist patterns",
      input: ["sharepoint.com"],
      expected: {
        hostnameAllowlist: ["sharepoint.com", "*.sharepoint.com"],
      },
    },
    {
      name: "normalizes wildcard prefixes, leading/trailing dots, and deduplicates patterns",
      input: ["*.TrafficManager.NET", ".trafficmanager.net.", " blob.core.windows.net "],
      expected: {
        hostnameAllowlist: [
          "trafficmanager.net",
          "*.trafficmanager.net",
          "blob.core.windows.net",
          "*.blob.core.windows.net",
        ],
      },
    },
  ])("$name", ({ input, expected }) => {
    expect(buildHostnameAllowlistPolicyFromSuffixAllowlist(input)).toEqual(expected);
  });
});
