import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Hono } from "hono";
import {
  authMiddleware,
  extractBearerToken,
  extractWsProtocolToken,
  getAuthToken,
  initAuthToken,
  isNoAuthMode,
  UnsafeNoAuthError,
  validateToken,
  validateWsOrigin,
  validateWsToken,
} from "./auth-middleware.js";
import { _resetConfigForTesting } from "./config.js";

// Mock logger to capture structured log calls
const mockWarn = vi.hoisted(() => vi.fn());
const mockError = vi.hoisted(() => vi.fn());
vi.mock("./logger.js", () => {
  const noop = () => {};
  const noopLogger = {
    info: noop,
    warn: mockWarn,
    error: mockError,
    debug: noop,
    trace: noop,
    fatal: noop,
    child: () => noopLogger,
  };
  return {
    logger: noopLogger,
    createLogger: () => noopLogger,
    createSessionLogger: () => noopLogger,
  };
});

describe("extractBearerToken", () => {
  it("extracts token from valid Bearer header", () => {
    expect(extractBearerToken("Bearer abc123")).toBe("abc123");
  });

  it("is case-insensitive for Bearer prefix", () => {
    expect(extractBearerToken("bearer abc123")).toBe("abc123");
    expect(extractBearerToken("BEARER abc123")).toBe("abc123");
  });

  it("returns null for missing header", () => {
    expect(extractBearerToken(undefined)).toBeNull();
    expect(extractBearerToken(null)).toBeNull();
    expect(extractBearerToken("")).toBeNull();
  });

  it("returns null for non-Bearer auth schemes", () => {
    expect(extractBearerToken("Basic abc123")).toBeNull();
    expect(extractBearerToken("Token abc123")).toBeNull();
  });

  it("returns null for malformed Bearer header", () => {
    expect(extractBearerToken("Bearer")).toBeNull();
    expect(extractBearerToken("Bearer ")).toBeNull();
  });
});

describe("extractWsProtocolToken", () => {
  it("extracts token from auth.TOKEN protocol", () => {
    expect(extractWsProtocolToken("zencode-protocol, auth.my-secret")).toBe("my-secret");
  });

  it("extracts token when auth protocol is first", () => {
    expect(extractWsProtocolToken("auth.abc123, zencode-protocol")).toBe("abc123");
  });

  it("extracts token from single auth protocol", () => {
    expect(extractWsProtocolToken("auth.token-value")).toBe("token-value");
  });

  it("returns null when no auth protocol present", () => {
    expect(extractWsProtocolToken("zencode-protocol")).toBeNull();
  });

  it("returns null for missing header", () => {
    expect(extractWsProtocolToken(undefined)).toBeNull();
    expect(extractWsProtocolToken(null)).toBeNull();
    expect(extractWsProtocolToken("")).toBeNull();
  });
});

describe("isNoAuthMode", () => {
  let prevNoAuth: string | undefined;

  beforeEach(() => {
    prevNoAuth = process.env.ZENCODE_NO_AUTH;
  });

  afterEach(() => {
    if (prevNoAuth === undefined) {
      delete process.env.ZENCODE_NO_AUTH;
    } else {
      process.env.ZENCODE_NO_AUTH = prevNoAuth;
    }
    _resetConfigForTesting();
  });

  it("returns false when neither env var nor CLI flag is set", () => {
    delete process.env.ZENCODE_NO_AUTH;
    _resetConfigForTesting();
    expect(isNoAuthMode()).toBe(false);
  });

  it("returns true when ZENCODE_NO_AUTH=1", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    _resetConfigForTesting();
    expect(isNoAuthMode()).toBe(true);
  });
});

describe("getAuthToken", () => {
  let prevToken: string | undefined;
  let prevNoAuth: string | undefined;

  beforeEach(() => {
    prevToken = process.env.ZENCODE_AUTH_TOKEN;
    prevNoAuth = process.env.ZENCODE_NO_AUTH;
    _resetConfigForTesting();
  });

  afterEach(() => {
    if (prevToken === undefined) {
      delete process.env.ZENCODE_AUTH_TOKEN;
    } else {
      process.env.ZENCODE_AUTH_TOKEN = prevToken;
    }
    if (prevNoAuth === undefined) {
      delete process.env.ZENCODE_NO_AUTH;
    } else {
      process.env.ZENCODE_NO_AUTH = prevNoAuth;
    }
    _resetConfigForTesting();
  });

  it("returns null when env var is not set", () => {
    delete process.env.ZENCODE_AUTH_TOKEN;
    delete process.env.ZENCODE_NO_AUTH;
    expect(getAuthToken()).toBeNull();
  });

  it("returns null when env var is empty string", () => {
    process.env.ZENCODE_AUTH_TOKEN = "";
    expect(getAuthToken()).toBeNull();
  });

  it("returns null when env var is whitespace only", () => {
    process.env.ZENCODE_AUTH_TOKEN = "   ";
    expect(getAuthToken()).toBeNull();
  });

  it("returns trimmed token when set", () => {
    process.env.ZENCODE_AUTH_TOKEN = " my-secret-token ";
    expect(getAuthToken()).toBe("my-secret-token");
  });

  it("returns null when no-auth mode is enabled even if token is set", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    process.env.ZENCODE_NO_AUTH = "1";
    _resetConfigForTesting();
    expect(getAuthToken()).toBeNull();
  });
});

describe("validateToken", () => {
  let prevToken: string | undefined;

  beforeEach(() => {
    prevToken = process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();
  });

  afterEach(() => {
    if (prevToken === undefined) {
      delete process.env.ZENCODE_AUTH_TOKEN;
    } else {
      process.env.ZENCODE_AUTH_TOKEN = prevToken;
    }
  });

  it("returns true when auth is not configured", () => {
    delete process.env.ZENCODE_AUTH_TOKEN;
    expect(validateToken(null)).toBe(true);
    expect(validateToken("anything")).toBe(true);
  });

  it("returns false when token is null and auth is configured", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    expect(validateToken(null)).toBe(false);
  });

  it("returns true for matching token", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    expect(validateToken("secret")).toBe(true);
  });

  it("returns false for non-matching token", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    expect(validateToken("wrong")).toBe(false);
  });
});

describe("validateWsToken", () => {
  let prevToken: string | undefined;

  beforeEach(() => {
    prevToken = process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();
  });

  afterEach(() => {
    if (prevToken === undefined) {
      delete process.env.ZENCODE_AUTH_TOKEN;
    } else {
      process.env.ZENCODE_AUTH_TOKEN = prevToken;
    }
  });

  it("passes when auth is not configured", () => {
    delete process.env.ZENCODE_AUTH_TOKEN;
    const url = new URL("ws://localhost:3456/ws/browser/abc");
    expect(validateWsToken(url, null)).toEqual({ valid: true });
  });

  it("fails when no token is provided at all", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const url = new URL("ws://localhost:3456/ws/browser/abc");
    const result = validateWsToken(url, null);
    expect(result.valid).toBe(false);
    expect(result.error).toContain("Authentication required");
  });

  it("passes when token is provided via Sec-WebSocket-Protocol header", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const url = new URL("ws://localhost:3456/ws/browser/abc");
    const result = validateWsToken(url, "zencode-protocol, auth.secret");
    expect(result.valid).toBe(true);
    expect(result.protocols).toEqual(["zencode-protocol"]);
  });

  it("fails when Sec-WebSocket-Protocol token is wrong", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const url = new URL("ws://localhost:3456/ws/browser/abc");
    const result = validateWsToken(url, "zencode-protocol, auth.wrong");
    expect(result.valid).toBe(false);
    expect(result.error).toContain("Invalid");
  });

  it("passes with legacy query param fallback", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const url = new URL("ws://localhost:3456/ws/browser/abc?token=secret");
    const result = validateWsToken(url, null);
    expect(result.valid).toBe(true);
  });

  it("logs a deprecation warning when query param token is used", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    mockWarn.mockClear();
    const url = new URL("ws://localhost:3456/ws/browser/abc?token=secret");
    validateWsToken(url, null);
    expect(mockWarn).toHaveBeenCalledOnce();
    expect(mockWarn).toHaveBeenCalledWith(
      expect.objectContaining({ deprecation: "ws-query-token" }),
      expect.stringContaining("deprecated"),
    );
  });

  it("fails when legacy query param token is wrong", () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const url = new URL("ws://localhost:3456/ws/browser/abc?token=wrong");
    const result = validateWsToken(url, null);
    expect(result.valid).toBe(false);
    expect(result.error).toContain("Invalid");
  });
});

describe("validateWsOrigin", () => {
  const allowedOrigins = [
    "http://localhost:5174",
    "http://localhost:3456",
    "http://127.0.0.1:3456",
  ];

  it("allows requests with no Origin header (non-browser clients)", () => {
    expect(validateWsOrigin(undefined, allowedOrigins)).toEqual({ valid: true });
    expect(validateWsOrigin(null, allowedOrigins)).toEqual({ valid: true });
  });

  it("allows requests from listed origins", () => {
    expect(validateWsOrigin("http://localhost:5174", allowedOrigins)).toEqual({ valid: true });
    expect(validateWsOrigin("http://localhost:3456", allowedOrigins)).toEqual({ valid: true });
    expect(validateWsOrigin("http://127.0.0.1:3456", allowedOrigins)).toEqual({ valid: true });
  });

  it("rejects requests from unlisted origins", () => {
    const result = validateWsOrigin("http://evil.example.com", allowedOrigins);
    expect(result.valid).toBe(false);
    expect(result.error).toContain("not allowed");
  });

  it("rejects requests from similar but different origins", () => {
    const result = validateWsOrigin("http://localhost:9999", allowedOrigins);
    expect(result.valid).toBe(false);
  });
});

describe("authMiddleware (Hono integration)", () => {
  let prevToken: string | undefined;

  beforeEach(() => {
    prevToken = process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();
  });

  afterEach(() => {
    if (prevToken === undefined) {
      delete process.env.ZENCODE_AUTH_TOKEN;
    } else {
      process.env.ZENCODE_AUTH_TOKEN = prevToken;
    }
  });

  function createApp(): Hono {
    const app = new Hono();
    app.use("/api/*", authMiddleware());
    app.get("/api/test", (c) => c.json({ ok: true }));
    return app;
  }

  it("allows requests when auth is not configured", async () => {
    delete process.env.ZENCODE_AUTH_TOKEN;
    const app = createApp();
    const res = await app.request("/api/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toEqual({ ok: true });
  });

  it("returns 401 when token is missing and auth is configured", async () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const app = createApp();
    const res = await app.request("/api/test");
    expect(res.status).toBe(401);
    const body = await res.json();
    expect(body.error).toContain("Authentication required");
  });

  it("returns 401 for invalid token", async () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const app = createApp();
    const res = await app.request("/api/test", {
      headers: { Authorization: "Bearer wrong-token" },
    });
    expect(res.status).toBe(401);
    const body = await res.json();
    expect(body.error).toContain("Invalid");
  });

  it("returns 401 for non-Bearer auth scheme", async () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const app = createApp();
    const res = await app.request("/api/test", {
      headers: { Authorization: "Basic dXNlcjpwYXNz" },
    });
    expect(res.status).toBe(401);
    const body = await res.json();
    expect(body.error).toContain("Authentication required");
  });

  it("allows requests with valid Bearer token", async () => {
    process.env.ZENCODE_AUTH_TOKEN = "secret";
    const app = createApp();
    const res = await app.request("/api/test", {
      headers: { Authorization: "Bearer secret" },
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toEqual({ ok: true });
  });

  it("handles token with leading/trailing whitespace in env", async () => {
    process.env.ZENCODE_AUTH_TOKEN = "  secret  ";
    const app = createApp();
    const res = await app.request("/api/test", {
      headers: { Authorization: "Bearer secret" },
    });
    expect(res.status).toBe(200);
  });
});

describe("initAuthToken (no-auth safeguards)", () => {
  let prevNoAuth: string | undefined;
  let prevHost: string | undefined;
  let prevUnsafe: string | undefined;
  let prevToken: string | undefined;

  beforeEach(() => {
    prevNoAuth = process.env.ZENCODE_NO_AUTH;
    prevHost = process.env.HOST;
    prevUnsafe = process.env.ZENCODE_UNSAFE_NO_AUTH;
    prevToken = process.env.ZENCODE_AUTH_TOKEN;
    mockError.mockClear();
  });

  afterEach(() => {
    for (const [key, val] of Object.entries({
      ZENCODE_NO_AUTH: prevNoAuth,
      HOST: prevHost,
      ZENCODE_UNSAFE_NO_AUTH: prevUnsafe,
      ZENCODE_AUTH_TOKEN: prevToken,
    })) {
      if (val === undefined) delete process.env[key];
      else process.env[key] = val;
    }
    _resetConfigForTesting();
  });

  it("logs at error level when --no-auth is used on localhost", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "localhost";
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    const result = initAuthToken();

    expect(result).toBeNull();
    expect(mockError).toHaveBeenCalledWith(
      expect.stringContaining("DISABLED"),
    );
  });

  it("allows --no-auth on 127.0.0.1 (localhost equivalent)", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "127.0.0.1";
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    const result = initAuthToken();

    expect(result).toBeNull();
    expect(mockError).toHaveBeenCalledWith(
      expect.stringContaining("DISABLED"),
    );
  });

  it("throws UnsafeNoAuthError when --no-auth + HOST=0.0.0.0", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "0.0.0.0";
    delete process.env.ZENCODE_UNSAFE_NO_AUTH;
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    expect(() => initAuthToken()).toThrow(UnsafeNoAuthError);
  });

  it("error message contains HOST value and remediation steps", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "0.0.0.0";
    delete process.env.ZENCODE_UNSAFE_NO_AUTH;
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    expect(() => initAuthToken()).toThrow(/HOST=0\.0\.0\.0/);
    try {
      initAuthToken();
    } catch (err) {
      expect((err as Error).message).toContain("ZENCODE_UNSAFE_NO_AUTH=1");
    }
  });

  it("throws for any non-localhost host without unsafe override", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "192.168.1.100";
    delete process.env.ZENCODE_UNSAFE_NO_AUTH;
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    expect(() => initAuthToken()).toThrow(UnsafeNoAuthError);
  });

  it("allows --no-auth + HOST=0.0.0.0 when ZENCODE_UNSAFE_NO_AUTH=1", () => {
    process.env.ZENCODE_NO_AUTH = "1";
    process.env.HOST = "0.0.0.0";
    process.env.ZENCODE_UNSAFE_NO_AUTH = "1";
    delete process.env.ZENCODE_AUTH_TOKEN;
    _resetConfigForTesting();

    const result = initAuthToken();

    expect(result).toBeNull();
    expect(mockError).toHaveBeenCalledWith(
      expect.stringContaining("DISABLED"),
    );
  });

  it("does not throw when auth is enabled (no --no-auth)", () => {
    delete process.env.ZENCODE_NO_AUTH;
    process.env.HOST = "0.0.0.0";
    process.env.ZENCODE_AUTH_TOKEN = "some-token";
    _resetConfigForTesting();

    const result = initAuthToken();

    expect(result).toBe("some-token");
  });
});
