import { describe, it, expect, beforeEach, afterEach } from "vitest";
import type { AppConfig } from "./config.js";
import { ConfigValidationError } from "./config.js";

// We need dynamic imports to test config parsing with different env states.
// Because the config module is parsed at import time, we re-import it after
// modifying process.env to test different scenarios.

/** Snapshot & restore all env vars we touch so tests are hermetic. */
const MANAGED_KEYS = [
  "PORT",
  "HOST",
  "NODE_ENV",
  "ZENCODE_NO_AUTH",
  "ZENCODE_AUTH_TOKEN",
  "ZENCODE_LOG_LEVEL",
  "ZENCODE_RATE_LIMIT",
  "ZENCODE_RATE_WINDOW_MS",
  "ZENCODE_RECORDINGS_DIR",
  "ZENCODE_RECORDINGS_MAX_LINES",
  "ZENCODE_RECORD",
  "ZENCODE_IDLE_TIMEOUT_SECONDS",
  "ZENCODE_RECONNECT_GRACE_MS",
  "ZENCODE_SESSION_DIR",
  "ZENCODE_CORS_ORIGINS",
  "ZENCODE_CONTAINER_SDK_HOST",
  "ZENCODE_FORCE_BYPASS_IN_CONTAINER",
  "ZENCODE_ALLOWED_PATHS",
  "ZENCODE_EDITOR_PORT",
  "ZENCODE_INIT_SCRIPT_TIMEOUT",
  "__ZENCODE_PACKAGE_ROOT",
  "CLAUDE_PROJECTS_DIR",
  "ZENCODE_TRUST_PROXY",
  "ZENCODE_UNSAFE_NO_AUTH",
] as const;

type EnvSnapshot = Record<string, string | undefined>;

function snapshotEnv(): EnvSnapshot {
  const snap: EnvSnapshot = {};
  for (const key of MANAGED_KEYS) {
    snap[key] = process.env[key];
  }
  return snap;
}

function restoreEnv(snap: EnvSnapshot): void {
  for (const [key, val] of Object.entries(snap)) {
    if (val === undefined) delete process.env[key];
    else process.env[key] = val;
  }
}

function clearManagedEnv(): void {
  for (const key of MANAGED_KEYS) {
    delete process.env[key];
  }
}

describe("config module", () => {
  let saved: EnvSnapshot;
  let _resetConfigForTesting: () => void;
  let getConfig: () => AppConfig;

  beforeEach(async () => {
    saved = snapshotEnv();
    clearManagedEnv();

    // Import fresh — _resetConfigForTesting is used to re-read env within the
    // same import without re-importing the whole module.
    const mod = await import("./config.js");
    _resetConfigForTesting = mod._resetConfigForTesting;
    getConfig = () => mod.config;
  });

  afterEach(() => {
    restoreEnv(saved);
  });

  // ── Defaults ────────────────────────────────────────────────────────────

  describe("defaults", () => {
    it("provides sensible defaults when no env vars are set", () => {
      _resetConfigForTesting();
      const c = getConfig();

      expect(c.host).toBe("localhost");
      // NODE_ENV is set to "test" by vitest.config.ts env option, so we can't
      // reliably test the development default here. That's fine — the schema
      // default is "development".
      expect(["development", "test"]).toContain(c.nodeEnv);
      expect(c.noAuth).toBe(false);
      expect(c.logLevel).toBe("info");
      expect(c.rateLimit).toBe(100);
      expect(c.rateWindowMs).toBe(15_000);
      expect(c.recordEnabled).toBe(true);
      expect(c.idleTimeoutSeconds).toBe(120);
      expect(c.reconnectGraceMs).toBe(30_000);
      expect(c.containerSdkHost).toBe("host.docker.internal");
      expect(c.forceBypassInContainer).toBe(false);
      expect(c.corsOrigins).toEqual([]);
      expect(c.allowedPaths).toEqual([]);
      expect(c.editorPort).toBe(13_338);
      expect(c.initScriptTimeout).toBe(120_000);
    });

    it("uses dev port when NODE_ENV is not production", () => {
      process.env.NODE_ENV = "development";
      _resetConfigForTesting();
      expect(getConfig().port).toBe(3457);
    });

    it("uses production port when NODE_ENV is production", () => {
      process.env.NODE_ENV = "production";
      _resetConfigForTesting();
      expect(getConfig().port).toBe(3456);
    });
  });

  // ── Server ──────────────────────────────────────────────────────────────

  describe("server config", () => {
    it("parses PORT as a number", () => {
      process.env.PORT = "8080";
      _resetConfigForTesting();
      expect(getConfig().port).toBe(8080);
    });

    it("parses HOST", () => {
      process.env.HOST = "0.0.0.0";
      _resetConfigForTesting();
      expect(getConfig().host).toBe("0.0.0.0");
    });

    it("sets isProduction correctly", () => {
      process.env.NODE_ENV = "production";
      _resetConfigForTesting();
      expect(getConfig().isProduction).toBe(true);

      process.env.NODE_ENV = "development";
      _resetConfigForTesting();
      expect(getConfig().isProduction).toBe(false);
    });
  });

  // ── Auth ────────────────────────────────────────────────────────────────

  describe("auth config", () => {
    it("parses ZENCODE_NO_AUTH=1 as true", () => {
      process.env.ZENCODE_NO_AUTH = "1";
      _resetConfigForTesting();
      expect(getConfig().noAuth).toBe(true);
    });

    it("parses ZENCODE_NO_AUTH=0 as false", () => {
      process.env.ZENCODE_NO_AUTH = "0";
      _resetConfigForTesting();
      expect(getConfig().noAuth).toBe(false);
    });

    it("trims ZENCODE_AUTH_TOKEN", () => {
      process.env.ZENCODE_AUTH_TOKEN = "  my-secret  ";
      _resetConfigForTesting();
      expect(getConfig().authToken).toBe("my-secret");
    });

    it("returns undefined for empty ZENCODE_AUTH_TOKEN", () => {
      process.env.ZENCODE_AUTH_TOKEN = "   ";
      _resetConfigForTesting();
      expect(getConfig().authToken).toBeUndefined();
    });

    it("parses ZENCODE_UNSAFE_NO_AUTH=1 as true", () => {
      process.env.ZENCODE_UNSAFE_NO_AUTH = "1";
      _resetConfigForTesting();
      expect(getConfig().unsafeNoAuth).toBe(true);
    });

    it("defaults ZENCODE_UNSAFE_NO_AUTH to false", () => {
      _resetConfigForTesting();
      expect(getConfig().unsafeNoAuth).toBe(false);
    });
  });

  // ── Rate limiting ──────────────────────────────────────────────────────

  describe("rate limiting config", () => {
    it("parses rate limit from env", () => {
      process.env.ZENCODE_RATE_LIMIT = "50";
      process.env.ZENCODE_RATE_WINDOW_MS = "60000";
      _resetConfigForTesting();
      expect(getConfig().rateLimit).toBe(50);
      expect(getConfig().rateWindowMs).toBe(60_000);
    });

    it("falls back to defaults when env vars are absent", () => {
      _resetConfigForTesting();
      expect(getConfig().rateLimit).toBe(100);
      expect(getConfig().rateWindowMs).toBe(15_000);
    });
  });

  // ── Recordings ──────────────────────────────────────────────────────────

  describe("recording config", () => {
    it("parses ZENCODE_RECORD=0 as false", () => {
      process.env.ZENCODE_RECORD = "0";
      _resetConfigForTesting();
      expect(getConfig().recordEnabled).toBe(false);
    });

    it("parses ZENCODE_RECORD=false as false", () => {
      process.env.ZENCODE_RECORD = "false";
      _resetConfigForTesting();
      expect(getConfig().recordEnabled).toBe(false);
    });

    it("parses ZENCODE_RECORD=1 as true", () => {
      process.env.ZENCODE_RECORD = "1";
      _resetConfigForTesting();
      expect(getConfig().recordEnabled).toBe(true);
    });

    it("picks up recordings dir", () => {
      process.env.ZENCODE_RECORDINGS_DIR = "/tmp/recordings";
      _resetConfigForTesting();
      expect(getConfig().recordingsDir).toBe("/tmp/recordings");
    });

    it("picks up recordings max lines", () => {
      process.env.ZENCODE_RECORDINGS_MAX_LINES = "50000";
      _resetConfigForTesting();
      expect(getConfig().recordingsMaxLines).toBe(50_000);
    });
  });

  // ── Comma-separated lists ──────────────────────────────────────────────

  describe("comma-separated values", () => {
    it("parses ZENCODE_CORS_ORIGINS", () => {
      process.env.ZENCODE_CORS_ORIGINS =
        "http://example.com, http://other.com , ";
      _resetConfigForTesting();
      expect(getConfig().corsOrigins).toEqual([
        "http://example.com",
        "http://other.com",
      ]);
    });

    it("parses ZENCODE_ALLOWED_PATHS", () => {
      process.env.ZENCODE_ALLOWED_PATHS = "/mnt/data,,/opt/projects,";
      _resetConfigForTesting();
      expect(getConfig().allowedPaths).toEqual(["/mnt/data", "/opt/projects"]);
    });

    it("returns empty array when env is not set", () => {
      _resetConfigForTesting();
      expect(getConfig().corsOrigins).toEqual([]);
      expect(getConfig().allowedPaths).toEqual([]);
    });
  });

  // ── Boolean flags ──────────────────────────────────────────────────────

  describe("boolean flags", () => {
    it("parses ZENCODE_FORCE_BYPASS_IN_CONTAINER=1 as true", () => {
      process.env.ZENCODE_FORCE_BYPASS_IN_CONTAINER = "1";
      _resetConfigForTesting();
      expect(getConfig().forceBypassInContainer).toBe(true);
    });

    it("defaults ZENCODE_FORCE_BYPASS_IN_CONTAINER to false", () => {
      _resetConfigForTesting();
      expect(getConfig().forceBypassInContainer).toBe(false);
    });
  });

  // ── Timeouts ────────────────────────────────────────────────────────────

  describe("timeout config", () => {
    it("parses idle timeout", () => {
      process.env.ZENCODE_IDLE_TIMEOUT_SECONDS = "60";
      _resetConfigForTesting();
      expect(getConfig().idleTimeoutSeconds).toBe(60);
    });

    it("parses reconnect grace ms", () => {
      process.env.ZENCODE_RECONNECT_GRACE_MS = "5000";
      _resetConfigForTesting();
      expect(getConfig().reconnectGraceMs).toBe(5000);
    });

    it("parses init script timeout", () => {
      process.env.ZENCODE_INIT_SCRIPT_TIMEOUT = "60000";
      _resetConfigForTesting();
      expect(getConfig().initScriptTimeout).toBe(60_000);
    });
  });

  // ── Internal / Claude ──────────────────────────────────────────────────

  describe("internal config", () => {
    it("picks up __ZENCODE_PACKAGE_ROOT", () => {
      process.env.__ZENCODE_PACKAGE_ROOT = "/opt/zencode";
      _resetConfigForTesting();
      expect(getConfig().packageRoot).toBe("/opt/zencode");
    });

    it("picks up CLAUDE_PROJECTS_DIR", () => {
      process.env.CLAUDE_PROJECTS_DIR = "/home/user/.claude/projects";
      _resetConfigForTesting();
      expect(getConfig().claudeProjectsDir).toBe(
        "/home/user/.claude/projects",
      );
    });
  });

  // ── Frozen ─────────────────────────────────────────────────────────────

  describe("immutability", () => {
    it("config object is frozen", () => {
      _resetConfigForTesting();
      const c = getConfig();
      expect(Object.isFrozen(c)).toBe(true);
    });

    it("cannot mutate config properties", () => {
      _resetConfigForTesting();
      const c = getConfig();
      expect(() => {
        (c as unknown as Record<string, unknown>).port = 9999;
      }).toThrow();
    });
  });

  // ── _resetConfigForTesting ─────────────────────────────────────────────

  describe("_resetConfigForTesting", () => {
    it("picks up env changes after reset", () => {
      process.env.PORT = "1111";
      _resetConfigForTesting();
      expect(getConfig().port).toBe(1111);

      process.env.PORT = "2222";
      // Before reset, old value persists
      expect(getConfig().port).toBe(1111);

      _resetConfigForTesting();
      expect(getConfig().port).toBe(2222);
    });
  });

  // ── Validation errors ─────────────────────────────────────────────────

  describe("validation errors", () => {
    it("throws on invalid NODE_ENV value", () => {
      process.env.NODE_ENV = "invalid_env";
      expect(() => _resetConfigForTesting()).toThrow(
        /Invalid environment configuration/,
      );
    });

    it("throws ConfigValidationError with issues array", () => {
      process.env.NODE_ENV = "invalid_env";
      try {
        _resetConfigForTesting();
        expect.unreachable("should have thrown");
      } catch (err) {
        expect(err).toBeInstanceOf(ConfigValidationError);
        const configErr = err as ConfigValidationError;
        expect(configErr.name).toBe("ConfigValidationError");
        expect(configErr.issues).toBeInstanceOf(Array);
        expect(configErr.issues.length).toBeGreaterThan(0);
      }
    });
  });
});
