import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Hono } from "hono";
import {
  RateLimiter,
  rateLimitMiddleware,
  getClientIp,
  createRateLimiterFromEnv,
} from "./rate-limiter.js";
import { _resetConfigForTesting } from "./config.js";

// ── RateLimiter class ───────────────────────────────────────────────────────

describe("RateLimiter", () => {
  let limiter: RateLimiter;

  beforeEach(() => {
    limiter = new RateLimiter(3, 1000); // 3 requests per 1 second
  });

  afterEach(() => {
    limiter.stopCleanup();
  });

  it("allows requests within the limit", () => {
    const r1 = limiter.check("1.2.3.4");
    expect(r1.allowed).toBe(true);
    expect(r1.remaining).toBe(2);

    const r2 = limiter.check("1.2.3.4");
    expect(r2.allowed).toBe(true);
    expect(r2.remaining).toBe(1);

    const r3 = limiter.check("1.2.3.4");
    expect(r3.allowed).toBe(true);
    expect(r3.remaining).toBe(0);
  });

  it("blocks requests exceeding the limit", () => {
    limiter.check("1.2.3.4");
    limiter.check("1.2.3.4");
    limiter.check("1.2.3.4");

    const r4 = limiter.check("1.2.3.4");
    expect(r4.allowed).toBe(false);
    expect(r4.remaining).toBe(0);
  });

  it("tracks different IPs independently", () => {
    limiter.check("1.2.3.4");
    limiter.check("1.2.3.4");
    limiter.check("1.2.3.4");

    // Different IP should still be allowed
    const result = limiter.check("5.6.7.8");
    expect(result.allowed).toBe(true);
    expect(result.remaining).toBe(2);
  });

  it("resets after window expires", () => {
    vi.useFakeTimers();
    try {
      limiter.check("1.2.3.4");
      limiter.check("1.2.3.4");
      limiter.check("1.2.3.4");

      const blocked = limiter.check("1.2.3.4");
      expect(blocked.allowed).toBe(false);

      // Advance past the window
      vi.advanceTimersByTime(1001);

      const afterReset = limiter.check("1.2.3.4");
      expect(afterReset.allowed).toBe(true);
      expect(afterReset.remaining).toBe(2);
    } finally {
      vi.useRealTimers();
    }
  });

  it("provides correct resetAt timestamp", () => {
    vi.useFakeTimers({ now: 10_000 });
    try {
      const result = limiter.check("1.2.3.4");
      expect(result.resetAt).toBe(11_000); // 10000 + 1000ms window
    } finally {
      vi.useRealTimers();
    }
  });

  it("reports limit in check results", () => {
    const result = limiter.check("1.2.3.4");
    expect(result.limit).toBe(3);
  });

  it("reports size correctly", () => {
    expect(limiter.size).toBe(0);
    limiter.check("1.2.3.4");
    expect(limiter.size).toBe(1);
    limiter.check("5.6.7.8");
    expect(limiter.size).toBe(2);
  });

  it("resets all state", () => {
    limiter.check("1.2.3.4");
    limiter.check("5.6.7.8");
    expect(limiter.size).toBe(2);

    limiter.reset();
    expect(limiter.size).toBe(0);

    // Should allow requests again
    const result = limiter.check("1.2.3.4");
    expect(result.allowed).toBe(true);
    expect(result.remaining).toBe(2);
  });
});

// ── purgeExpired ────────────────────────────────────────────────────────────

describe("RateLimiter.purgeExpired", () => {
  it("removes expired entries and keeps active ones", () => {
    vi.useFakeTimers({ now: 10_000 });
    try {
      const limiter = new RateLimiter(5, 1000);
      limiter.check("expired-ip");

      // Advance so first entry expires, then add a new one
      vi.advanceTimersByTime(1500);
      limiter.check("active-ip");

      expect(limiter.size).toBe(2);
      limiter.purgeExpired();
      expect(limiter.size).toBe(1);

      // The active one should still work
      const result = limiter.check("active-ip");
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(3); // 5 - 2 (first check + this check)
    } finally {
      vi.useRealTimers();
    }
  });

  it("startCleanup triggers periodic purge", () => {
    vi.useFakeTimers({ now: 10_000 });
    try {
      const limiter = new RateLimiter(5, 500);
      limiter.check("ip1");

      // Advance past the window so entry is expired
      vi.advanceTimersByTime(600);
      expect(limiter.size).toBe(1);

      // Start cleanup with a short interval
      limiter.startCleanup(200);

      // Trigger the cleanup interval
      vi.advanceTimersByTime(200);
      expect(limiter.size).toBe(0);

      limiter.stopCleanup();
    } finally {
      vi.useRealTimers();
    }
  });
});

// ── createRateLimiterFromEnv ────────────────────────────────────────────────

describe("createRateLimiterFromEnv", () => {
  let prevLimit: string | undefined;
  let prevWindow: string | undefined;

  beforeEach(() => {
    prevLimit = process.env.ZENCODE_RATE_LIMIT;
    prevWindow = process.env.ZENCODE_RATE_WINDOW_MS;
  });

  afterEach(() => {
    if (prevLimit === undefined) delete process.env.ZENCODE_RATE_LIMIT;
    else process.env.ZENCODE_RATE_LIMIT = prevLimit;

    if (prevWindow === undefined) delete process.env.ZENCODE_RATE_WINDOW_MS;
    else process.env.ZENCODE_RATE_WINDOW_MS = prevWindow;
  });

  it("uses defaults when env vars are not set", () => {
    delete process.env.ZENCODE_RATE_LIMIT;
    delete process.env.ZENCODE_RATE_WINDOW_MS;
    _resetConfigForTesting();
    const limiter = createRateLimiterFromEnv();
    // Default: 100 requests — make 100 requests and verify they all pass
    for (let i = 0; i < 100; i++) {
      expect(limiter.check("test").allowed).toBe(true);
    }
    expect(limiter.check("test").allowed).toBe(false);
  });

  it("respects ZENCODE_RATE_LIMIT env var", () => {
    process.env.ZENCODE_RATE_LIMIT = "5";
    delete process.env.ZENCODE_RATE_WINDOW_MS;
    _resetConfigForTesting();
    const limiter = createRateLimiterFromEnv();
    for (let i = 0; i < 5; i++) {
      expect(limiter.check("test").allowed).toBe(true);
    }
    expect(limiter.check("test").allowed).toBe(false);
  });

  it("respects ZENCODE_RATE_WINDOW_MS env var", () => {
    vi.useFakeTimers({ now: 10_000 });
    try {
      process.env.ZENCODE_RATE_LIMIT = "2";
      process.env.ZENCODE_RATE_WINDOW_MS = "500";
      _resetConfigForTesting();
      const limiter = createRateLimiterFromEnv();

      limiter.check("test");
      limiter.check("test");
      expect(limiter.check("test").allowed).toBe(false);

      // Window should be 500ms
      vi.advanceTimersByTime(501);
      expect(limiter.check("test").allowed).toBe(true);
    } finally {
      vi.useRealTimers();
    }
  });
});

// ── getClientIp ─────────────────────────────────────────────────────────────

describe("getClientIp", () => {
  let prevTrustProxy: string | undefined;

  function makeContext(headers: Record<string, string> = {}, env: Record<string, unknown> = {}): any {
    return {
      req: {
        header(name: string) {
          // case-insensitive lookup
          const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
          return key ? headers[key] : undefined;
        },
      },
      env,
    };
  }

  beforeEach(() => {
    prevTrustProxy = process.env.ZENCODE_TRUST_PROXY;
  });

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

  it("ignores X-Forwarded-For when trustProxy is disabled (default)", () => {
    delete process.env.ZENCODE_TRUST_PROXY;
    _resetConfigForTesting();
    const ctx = makeContext({ "X-Forwarded-For": "10.0.0.1, 172.16.0.1" }, { remoteAddress: "127.0.0.1" });
    expect(getClientIp(ctx)).toBe("127.0.0.1");
  });

  it("extracts IP from X-Forwarded-For when trustProxy is enabled", () => {
    process.env.ZENCODE_TRUST_PROXY = "1";
    _resetConfigForTesting();
    const ctx = makeContext({ "X-Forwarded-For": "10.0.0.1, 172.16.0.1" });
    expect(getClientIp(ctx)).toBe("10.0.0.1");
  });

  it("uses single X-Forwarded-For value when trustProxy is enabled", () => {
    process.env.ZENCODE_TRUST_PROXY = "1";
    _resetConfigForTesting();
    const ctx = makeContext({ "X-Forwarded-For": "192.168.1.5" });
    expect(getClientIp(ctx)).toBe("192.168.1.5");
  });

  it("falls back to env.remoteAddress", () => {
    const ctx = makeContext({}, { remoteAddress: "127.0.0.1" });
    expect(getClientIp(ctx)).toBe("127.0.0.1");
  });

  it("falls back to env.ip", () => {
    const ctx = makeContext({}, { ip: "10.0.0.2" });
    expect(getClientIp(ctx)).toBe("10.0.0.2");
  });

  it("returns 'unknown' when no IP info available", () => {
    const ctx = makeContext({}, {});
    expect(getClientIp(ctx)).toBe("unknown");
  });
});

// ── rateLimitMiddleware (Hono integration) ──────────────────────────────────

describe("rateLimitMiddleware (Hono integration)", () => {
  function createApp(limiter: RateLimiter): Hono {
    const app = new Hono();
    app.use("/api/*", rateLimitMiddleware(limiter));
    app.get("/api/test", (c) => c.json({ ok: true }));
    app.get("/health", (c) => c.json({ status: "ok" }));
    return app;
  }

  it("allows requests under the limit and sets rate-limit headers", async () => {
    const limiter = new RateLimiter(3, 10_000);
    const app = createApp(limiter);

    const res = await app.request("/api/test");
    expect(res.status).toBe(200);
    expect(res.headers.get("X-RateLimit-Limit")).toBe("3");
    expect(res.headers.get("X-RateLimit-Remaining")).toBe("2");
    expect(res.headers.get("X-RateLimit-Reset")).toBeTruthy();
  });

  it("returns 429 when limit is exceeded", async () => {
    const limiter = new RateLimiter(2, 10_000);
    const app = createApp(limiter);

    await app.request("/api/test");
    await app.request("/api/test");

    const res = await app.request("/api/test");
    expect(res.status).toBe(429);

    const body = await res.json();
    expect(body.error).toContain("Too many requests");

    expect(res.headers.get("Retry-After")).toBeTruthy();
    expect(Number(res.headers.get("Retry-After"))).toBeGreaterThanOrEqual(1);
    expect(res.headers.get("X-RateLimit-Remaining")).toBe("0");
  });

  it("does not affect non-API routes", async () => {
    const limiter = new RateLimiter(1, 10_000);
    const app = createApp(limiter);

    // Exhaust the limit on /api
    await app.request("/api/test");
    const blocked = await app.request("/api/test");
    expect(blocked.status).toBe(429);

    // /health should not be rate-limited (middleware only on /api/*)
    const health = await app.request("/health");
    expect(health.status).toBe(200);
  });

  it("tracks different IPs separately via X-Forwarded-For when trustProxy is enabled", async () => {
    const prevTrustProxy = process.env.ZENCODE_TRUST_PROXY;
    process.env.ZENCODE_TRUST_PROXY = "1";
    _resetConfigForTesting();
    try {
      const limiter = new RateLimiter(1, 10_000);
      const app = createApp(limiter);

      const res1 = await app.request("/api/test", {
        headers: { "X-Forwarded-For": "10.0.0.1" },
      });
      expect(res1.status).toBe(200);

      // Same IP should be blocked
      const res2 = await app.request("/api/test", {
        headers: { "X-Forwarded-For": "10.0.0.1" },
      });
      expect(res2.status).toBe(429);

      // Different IP should still be allowed
      const res3 = await app.request("/api/test", {
        headers: { "X-Forwarded-For": "10.0.0.2" },
      });
      expect(res3.status).toBe(200);
    } finally {
      if (prevTrustProxy === undefined) delete process.env.ZENCODE_TRUST_PROXY;
      else process.env.ZENCODE_TRUST_PROXY = prevTrustProxy;
      _resetConfigForTesting();
    }
  });
});
