import { describe, expect, it, vi } from "vitest";
import {
  CONSENT_UPLOAD_HOST_ALLOWLIST,
  isPrivateOrReservedIP,
  uploadToConsentUrl,
  validateConsentUploadUrl,
} from "./file-consent.js";

// Helper: a resolveFn that returns a public IP by default
const publicResolve = async () => ({ address: "13.107.136.10" });
// Helper: a resolveFn that returns a private IP
const privateResolve = (ip: string) => async () => ({ address: ip });
// Helper: a resolveFn that returns multiple addresses
const multiResolve = (ips: string[]) => async () => ips.map((address) => ({ address }));
// Helper: a resolveFn that fails
const failingResolve = async () => {
  throw new Error("DNS failure");
};

// ─── isPrivateOrReservedIP ───────────────────────────────────────────────────

describe("isPrivateOrReservedIP", () => {
  it.each([
    ["10.0.0.1", true],
    ["10.255.255.255", true],
    ["172.16.0.1", true],
    ["172.31.255.255", true],
    ["172.15.0.1", false],
    ["172.32.0.1", false],
    ["192.168.0.1", true],
    ["192.168.255.255", true],
    ["127.0.0.1", true],
    ["127.255.255.255", true],
    ["169.254.0.1", true],
    ["169.254.169.254", true],
    ["0.0.0.0", true],
    ["8.8.8.8", false],
    ["13.107.136.10", false],
    ["52.96.0.1", false],
  ] as const)("IPv4 %s → %s", (ip, expected) => {
    expect(isPrivateOrReservedIP(ip)).toBe(expected);
  });

  it.each([
    ["::1", true],
    ["::", true],
    ["fe80::1", true],
    ["fe80::", true],
    ["fc00::1", true],
    ["fd12:3456::1", true],
    ["2001:0db8::1", false],
    ["2620:1ec:c11::200", false],
    // IPv4-mapped IPv6 addresses
    ["::ffff:127.0.0.1", true],
    ["::ffff:10.0.0.1", true],
    ["::ffff:192.168.1.1", true],
    ["::ffff:169.254.169.254", true],
    ["::ffff:8.8.8.8", false],
    ["::ffff:13.107.136.10", false],
  ] as const)("IPv6 %s → %s", (ip, expected) => {
    expect(isPrivateOrReservedIP(ip)).toBe(expected);
  });

  it.each([
    ["999.999.999.999", false],
    ["256.0.0.1", false],
    ["10.0.0.256", false],
    ["-1.0.0.1", false],
    ["1.2.3.4.5", false],
  ] as const)("malformed IPv4 %s → %s", (ip, expected) => {
    expect(isPrivateOrReservedIP(ip)).toBe(expected);
  });
});

// ─── validateConsentUploadUrl ────────────────────────────────────────────────

describe("validateConsentUploadUrl", () => {
  it("accepts a valid SharePoint HTTPS URL", async () => {
    await expect(
      validateConsentUploadUrl("https://contoso.sharepoint.com/sites/uploads/file.pdf", {
        resolveFn: publicResolve,
      }),
    ).resolves.toBeUndefined();
  });

  it("accepts subdomains of allowlisted domains", async () => {
    await expect(
      validateConsentUploadUrl(
        "https://contoso-my.sharepoint.com/personal/user/Documents/file.docx",
        { resolveFn: publicResolve },
      ),
    ).resolves.toBeUndefined();
  });

  it("accepts graph.microsoft.com", async () => {
    await expect(
      validateConsentUploadUrl("https://graph.microsoft.com/v1.0/me/drive/items/123/content", {
        resolveFn: publicResolve,
      }),
    ).resolves.toBeUndefined();
  });

  it("rejects non-HTTPS URLs", async () => {
    await expect(
      validateConsentUploadUrl("http://contoso.sharepoint.com/file.pdf", {
        resolveFn: publicResolve,
      }),
    ).rejects.toThrow("must use HTTPS");
  });

  it("rejects invalid URLs", async () => {
    await expect(
      validateConsentUploadUrl("not a url", { resolveFn: publicResolve }),
    ).rejects.toThrow("not a valid URL");
  });

  it("rejects hosts not in the allowlist", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.example.com/exfil", { resolveFn: publicResolve }),
    ).rejects.toThrow("not in the allowed domains");
  });

  it("rejects an SSRF attempt with internal metadata URL", async () => {
    await expect(
      validateConsentUploadUrl("https://169.254.169.254/latest/meta-data/", {
        resolveFn: publicResolve,
      }),
    ).rejects.toThrow("not in the allowed domains");
  });

  it("rejects localhost", async () => {
    await expect(
      validateConsentUploadUrl("https://localhost:8080/internal", { resolveFn: publicResolve }),
    ).rejects.toThrow("not in the allowed domains");
  });

  it("rejects when DNS resolves to a private IPv4 (10.x)", async () => {
    await expect(
      validateConsentUploadUrl("https://malicious.sharepoint.com/exfil", {
        resolveFn: privateResolve("10.0.0.1"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when DNS resolves to loopback", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: privateResolve("127.0.0.1"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when DNS resolves to link-local (169.254.x.x)", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: privateResolve("169.254.169.254"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when DNS resolves to IPv6 loopback", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: privateResolve("::1"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when DNS resolves to IPv4-mapped IPv6 private address", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: privateResolve("::ffff:10.0.0.1"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when DNS resolves to IPv4-mapped IPv6 loopback", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: privateResolve("::ffff:127.0.0.1"),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("rejects when any DNS answer is private/reserved", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: multiResolve(["13.107.136.10", "10.0.0.1"]),
      }),
    ).rejects.toThrow("private/reserved IP");
  });

  it("accepts when all DNS answers are public", async () => {
    await expect(
      validateConsentUploadUrl("https://evil.sharepoint.com/path", {
        resolveFn: multiResolve(["13.107.136.10", "52.96.0.1"]),
      }),
    ).resolves.toBeUndefined();
  });

  it("rejects when DNS resolution fails", async () => {
    await expect(
      validateConsentUploadUrl("https://nonexistent.sharepoint.com/path", {
        resolveFn: failingResolve,
      }),
    ).rejects.toThrow("Failed to resolve");
  });

  it("accepts a custom allowlist", async () => {
    await expect(
      validateConsentUploadUrl("https://custom.example.org/file", {
        allowlist: ["example.org"],
        resolveFn: publicResolve,
      }),
    ).resolves.toBeUndefined();
  });

  it("rejects hosts that are suffix-tricked (e.g. notsharepoint.com)", async () => {
    await expect(
      validateConsentUploadUrl("https://notsharepoint.com/file", { resolveFn: publicResolve }),
    ).rejects.toThrow("not in the allowed domains");
  });

  it("rejects file:// protocol", async () => {
    await expect(
      validateConsentUploadUrl("file:///etc/passwd", { resolveFn: publicResolve }),
    ).rejects.toThrow("must use HTTPS");
  });
});

// ─── CONSENT_UPLOAD_HOST_ALLOWLIST ───────────────────────────────────────────

describe("CONSENT_UPLOAD_HOST_ALLOWLIST", () => {
  it("contains only Microsoft/SharePoint domains", () => {
    for (const domain of CONSENT_UPLOAD_HOST_ALLOWLIST) {
      expect(
        domain.includes("microsoft") ||
          domain.includes("sharepoint") ||
          domain.includes("onedrive") ||
          domain.includes("1drv") ||
          domain.includes("live.com"),
      ).toBe(true);
    }
  });

  it("does not contain overly broad domains", () => {
    const broad = [
      "microsoft.com",
      "azure.com",
      "blob.core.windows.net",
      "azureedge.net",
      "trafficmanager.net",
    ];
    for (const domain of broad) {
      expect(CONSENT_UPLOAD_HOST_ALLOWLIST).not.toContain(domain);
    }
  });
});

// ─── uploadToConsentUrl (integration with validation) ────────────────────────

describe("uploadToConsentUrl", () => {
  it("sends the OpenClaw User-Agent header with consent uploads", async () => {
    const fetchFn = vi.fn(async () => new Response(null, { status: 200 }));

    await uploadToConsentUrl({
      url: "https://contoso.sharepoint.com/upload",
      buffer: Buffer.from("hello"),
      fetchFn,
      validationOpts: { resolveFn: publicResolve },
    });

    expect(fetchFn).toHaveBeenCalledWith(
      "https://contoso.sharepoint.com/upload",
      expect.objectContaining({
        method: "PUT",
        headers: expect.objectContaining({
          "Content-Range": "bytes 0-4/5",
          "Content-Type": "application/octet-stream",
          "User-Agent": expect.stringMatching(/^teams\.ts\[apps\]\/.+ OpenClaw\/.+$/),
        }),
      }),
    );
  });

  it("blocks upload to a disallowed host", async () => {
    const mockFetch = vi.fn();
    await expect(
      uploadToConsentUrl({
        url: "https://evil.example.com/exfil",
        buffer: Buffer.from("secret data"),
        fetchFn: mockFetch,
        validationOpts: { resolveFn: publicResolve },
      }),
    ).rejects.toThrow("not in the allowed domains");

    expect(mockFetch).not.toHaveBeenCalled();
  });

  it("blocks upload to a private IP", async () => {
    const mockFetch = vi.fn();
    await expect(
      uploadToConsentUrl({
        url: "https://compromised.sharepoint.com/upload",
        buffer: Buffer.from("data"),
        fetchFn: mockFetch,
        validationOpts: { resolveFn: privateResolve("10.0.0.1") },
      }),
    ).rejects.toThrow("private/reserved IP");

    expect(mockFetch).not.toHaveBeenCalled();
  });

  it("allows upload to a valid SharePoint URL and performs PUT", async () => {
    const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200 });
    const buffer = Buffer.from("file content");

    await uploadToConsentUrl({
      url: "https://contoso.sharepoint.com/sites/uploads/file.pdf",
      buffer,
      contentType: "application/pdf",
      fetchFn: mockFetch,
      validationOpts: { resolveFn: publicResolve },
    });

    expect(mockFetch).toHaveBeenCalledOnce();
    const [url, opts] = mockFetch.mock.calls[0];
    expect(url).toBe("https://contoso.sharepoint.com/sites/uploads/file.pdf");
    expect(opts.method).toBe("PUT");
    expect(opts.headers["Content-Type"]).toBe("application/pdf");
  });

  it("throws on non-OK response after passing validation", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 403,
      statusText: "Forbidden",
    });

    await expect(
      uploadToConsentUrl({
        url: "https://contoso.sharepoint.com/sites/uploads/file.pdf",
        buffer: Buffer.from("data"),
        fetchFn: mockFetch,
        validationOpts: { resolveFn: publicResolve },
      }),
    ).rejects.toThrow("File upload to consent URL failed: 403 Forbidden");
  });

  it("blocks HTTP (non-HTTPS) upload before fetch is called", async () => {
    const mockFetch = vi.fn();
    await expect(
      uploadToConsentUrl({
        url: "http://contoso.sharepoint.com/upload",
        buffer: Buffer.from("data"),
        fetchFn: mockFetch,
        validationOpts: { resolveFn: publicResolve },
      }),
    ).rejects.toThrow("must use HTTPS");

    expect(mockFetch).not.toHaveBeenCalled();
  });
});
