import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
  readMaskedConfig,
  resolveProviderConfig,
  writeConfig,
} from '../src/media-config.js';

const TEST_NANOBANANA_BASE_URL = 'https://nano-banana-gateway.example.test';

const OPENAI_ENV_KEYS = [
  'OD_OPENAI_API_KEY',
  'OPENAI_API_KEY',
  'AZURE_API_KEY',
  'AZURE_OPENAI_API_KEY',
];

describe('media-config OpenAI OAuth fallback', () => {
  let homeDir: string;
  let projectRoot: string;
  const originalHome = process.env.HOME;
  const originalEnv = Object.fromEntries(
    OPENAI_ENV_KEYS.map((key) => [key, process.env[key]]),
  );
  const originalMediaConfigDir = process.env.OD_MEDIA_CONFIG_DIR;
  const originalDataDir = process.env.OD_DATA_DIR;

  beforeEach(async () => {
    homeDir = await mkdtemp(path.join(tmpdir(), 'od-media-home-'));
    projectRoot = await mkdtemp(path.join(tmpdir(), 'od-media-project-'));
    process.env.HOME = homeDir;
    for (const key of OPENAI_ENV_KEYS) {
      delete process.env[key];
    }
    delete process.env.OD_MEDIA_CONFIG_DIR;
    delete process.env.OD_DATA_DIR;
  });

  afterEach(async () => {
    if (originalHome == null) {
      delete process.env.HOME;
    } else {
      process.env.HOME = originalHome;
    }
    for (const key of OPENAI_ENV_KEYS) {
      if (originalEnv[key] == null) {
        delete process.env[key];
      } else {
        process.env[key] = originalEnv[key];
      }
    }
    if (originalMediaConfigDir == null) {
      delete process.env.OD_MEDIA_CONFIG_DIR;
    } else {
      process.env.OD_MEDIA_CONFIG_DIR = originalMediaConfigDir;
    }
    if (originalDataDir == null) {
      delete process.env.OD_DATA_DIR;
    } else {
      process.env.OD_DATA_DIR = originalDataDir;
    }
    await rm(homeDir, { recursive: true, force: true });
    await rm(projectRoot, { recursive: true, force: true });
  });

  async function writeHomeJson(relPath: string, data: unknown) {
    const file = path.join(homeDir, relPath);
    await mkdir(path.dirname(file), { recursive: true });
    await writeFile(file, JSON.stringify(data), 'utf8');
  }

  async function writeStoredMediaConfig(data: unknown) {
    const file = path.join(projectRoot, '.od', 'media-config.json');
    await mkdir(path.dirname(file), { recursive: true });
    await writeFile(file, JSON.stringify(data), 'utf8');
  }

  function openaiProvider(masked: { providers: unknown }) {
    return (masked.providers as Record<string, unknown>).openai;
  }

  it('uses Hermes openai-codex OAuth when no API key is configured', async () => {
    await writeHomeJson('.hermes/auth.json', {
      providers: {
        'openai-codex': {
          tokens: { access_token: 'hermes-oauth-token' },
        },
      },
    });

    const resolved = await resolveProviderConfig(projectRoot, 'openai');
    const masked = await readMaskedConfig(projectRoot);

    expect(resolved.apiKey).toBe('hermes-oauth-token');
    expect(openaiProvider(masked)).toMatchObject({
      configured: true,
      source: 'oauth-hermes',
      apiKeyTail: '',
    });
  });

  it('uses Codex OAuth when Hermes has no OpenAI Codex credential', async () => {
    await writeHomeJson('.codex/auth.json', {
      tokens: { access_token: 'codex-oauth-token' },
    });

    const resolved = await resolveProviderConfig(projectRoot, 'openai');
    const masked = await readMaskedConfig(projectRoot);

    expect(resolved.apiKey).toBe('codex-oauth-token');
    expect(openaiProvider(masked)).toMatchObject({
      configured: true,
      source: 'oauth-codex',
      apiKeyTail: '',
    });
  });

  it('keeps stored provider config ahead of OAuth fallbacks', async () => {
    await writeHomeJson('.hermes/auth.json', {
      providers: {
        'openai-codex': {
          tokens: { access_token: 'hermes-oauth-token' },
        },
      },
    });
    await writeStoredMediaConfig({
      providers: {
        openai: {
          apiKey: 'stored-openai-key',
          baseUrl: 'https://example.test/v1',
        },
      },
    });

    const resolved = await resolveProviderConfig(projectRoot, 'openai');
    const masked = await readMaskedConfig(projectRoot);

    expect(resolved).toEqual({
      apiKey: 'stored-openai-key',
      baseUrl: 'https://example.test/v1',
    });
    expect(openaiProvider(masked)).toMatchObject({
      configured: true,
      source: 'stored',
      apiKeyTail: '-key',
      baseUrl: 'https://example.test/v1',
    });
  });

  it('resolves Nano Banana env and stored model overrides', async () => {
    process.env.OD_NANOBANANA_API_KEY = 'env-nano-key';
    await writeStoredMediaConfig({
      providers: {
        nanobanana: {
          apiKey: 'stored-nano-key',
          baseUrl: TEST_NANOBANANA_BASE_URL,
          model: 'gemini-3.1-flash-image-preview-custom',
        },
      },
    });

    const resolved = await resolveProviderConfig(projectRoot, 'nanobanana');
    const masked = await readMaskedConfig(projectRoot);
    const provider = (masked.providers as Record<string, unknown>).nanobanana;

    expect(resolved).toEqual({
      apiKey: 'env-nano-key',
      baseUrl: TEST_NANOBANANA_BASE_URL,
      model: 'gemini-3.1-flash-image-preview-custom',
    });
    expect(provider).toMatchObject({
      configured: true,
      source: 'env',
      apiKeyTail: '-key',
      baseUrl: TEST_NANOBANANA_BASE_URL,
      model: 'gemini-3.1-flash-image-preview-custom',
    });

    delete process.env.OD_NANOBANANA_API_KEY;
  });

  describe('OD_MEDIA_CONFIG_DIR / OD_DATA_DIR storage routing', () => {
    let overrideRoot: string;
    let originalMediaConfigDir: string | undefined;
    let originalDataDir: string | undefined;

    beforeEach(async () => {
      overrideRoot = await mkdtemp(path.join(tmpdir(), 'od-media-override-'));
      originalMediaConfigDir = process.env.OD_MEDIA_CONFIG_DIR;
      originalDataDir = process.env.OD_DATA_DIR;
      delete process.env.OD_MEDIA_CONFIG_DIR;
      delete process.env.OD_DATA_DIR;
    });

    afterEach(async () => {
      if (originalMediaConfigDir == null) {
        delete process.env.OD_MEDIA_CONFIG_DIR;
      } else {
        process.env.OD_MEDIA_CONFIG_DIR = originalMediaConfigDir;
      }
      if (originalDataDir == null) {
        delete process.env.OD_DATA_DIR;
      } else {
        process.env.OD_DATA_DIR = originalDataDir;
      }
      await rm(overrideRoot, { recursive: true, force: true });
    });

    async function writeProvidersAt(dir: string, data: unknown) {
      await mkdir(dir, { recursive: true });
      await writeFile(
        path.join(dir, 'media-config.json'),
        JSON.stringify(data),
        'utf8',
      );
    }

    it('reads media-config.json from an absolute OD_MEDIA_CONFIG_DIR', async () => {
      process.env.OD_MEDIA_CONFIG_DIR = overrideRoot;
      await writeProvidersAt(overrideRoot, {
        providers: {
          openai: {
            apiKey: 'absolute-key',
            baseUrl: 'https://absolute.test/v1',
          },
        },
      });

      const resolved = await resolveProviderConfig(projectRoot, 'openai');
      expect(resolved).toEqual({
        apiKey: 'absolute-key',
        baseUrl: 'https://absolute.test/v1',
      });
    });

    it('expands a leading ~/ against the user home directory', async () => {
      // Per-test HOME points at a tmpdir (set by outer beforeEach), so the
      // expansion lands somewhere safe to write.
      const subdir = '.od-test';
      process.env.OD_MEDIA_CONFIG_DIR = `~/${subdir}`;
      const expandedDir = path.join(homeDir, subdir);
      await writeProvidersAt(expandedDir, {
        providers: {
          openai: {
            apiKey: 'tilde-key',
            baseUrl: 'https://tilde.test/v1',
          },
        },
      });

      const resolved = await resolveProviderConfig(projectRoot, 'openai');
      expect(resolved).toEqual({
        apiKey: 'tilde-key',
        baseUrl: 'https://tilde.test/v1',
      });
    });

    it('resolves a relative override against projectRoot, not process.cwd', async () => {
      // process.cwd() during tests is typically the workspace root, which
      // is unrelated to the per-test projectRoot. A relative override must
      // land inside projectRoot, mirroring how resolveDataDir() in
      // server.ts anchors OD_DATA_DIR.
      const relative = 'config/media';
      process.env.OD_MEDIA_CONFIG_DIR = relative;
      const anchoredDir = path.join(projectRoot, relative);
      await writeProvidersAt(anchoredDir, {
        providers: {
          openai: {
            apiKey: 'relative-key',
            baseUrl: 'https://relative.test/v1',
          },
        },
      });

      const resolved = await resolveProviderConfig(projectRoot, 'openai');
      expect(resolved).toEqual({
        apiKey: 'relative-key',
        baseUrl: 'https://relative.test/v1',
      });
    });

    it('falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR is unset', async () => {
      // Packaged daemon (apps/packaged/src/sidecars.ts) and the
      // Home Manager / NixOS modules already set OD_DATA_DIR for the
      // rest of the daemon's runtime state. media-config should
      // co-locate there without needing a second env var.
      process.env.OD_DATA_DIR = overrideRoot;
      await writeProvidersAt(overrideRoot, {
        providers: {
          openai: {
            apiKey: 'datadir-key',
            baseUrl: 'https://datadir.test/v1',
          },
        },
      });

      const resolved = await resolveProviderConfig(projectRoot, 'openai');
      expect(resolved).toEqual({
        apiKey: 'datadir-key',
        baseUrl: 'https://datadir.test/v1',
      });
    });

    it('OD_MEDIA_CONFIG_DIR takes precedence over OD_DATA_DIR', async () => {
      const dataDir = await mkdtemp(path.join(tmpdir(), 'od-media-data-'));
      try {
        process.env.OD_DATA_DIR = dataDir;
        process.env.OD_MEDIA_CONFIG_DIR = overrideRoot;
        // Two competing files; only the OD_MEDIA_CONFIG_DIR one should
        // be read.
        await writeProvidersAt(dataDir, {
          providers: {
            openai: { apiKey: 'data-key', baseUrl: 'https://data/v1' },
          },
        });
        await writeProvidersAt(overrideRoot, {
          providers: {
            openai: { apiKey: 'media-key', baseUrl: 'https://media/v1' },
          },
        });

        const resolved = await resolveProviderConfig(projectRoot, 'openai');
        expect(resolved).toEqual({
          apiKey: 'media-key',
          baseUrl: 'https://media/v1',
        });
      } finally {
        await rm(dataDir, { recursive: true, force: true });
      }
    });

    it('writeConfig creates the override directory tree on first write', async () => {
      // Reproduces the actual user-reported failure mode: the override
      // directory does not exist yet (first launch on a read-only
      // install root), so writeConfig must mkdir -p before writing.
      // Without recursive mkdir + a writable override, this would
      // surface as ENOENT/EROFS to PUT /api/media/config.
      const target = path.join(overrideRoot, 'nested', 'inner');
      process.env.OD_MEDIA_CONFIG_DIR = target;

      await writeConfig(projectRoot, {
        providers: {
          openai: {
            apiKey: 'fresh-write-key',
            baseUrl: 'https://fresh.test/v1',
          },
        },
      });

      // File materialised at the override path.
      const onDisk = await readFile(
        path.join(target, 'media-config.json'),
        'utf8',
      );
      expect(JSON.parse(onDisk)).toEqual({
        providers: {
          openai: {
            apiKey: 'fresh-write-key',
            baseUrl: 'https://fresh.test/v1',
          },
        },
      });

      // And resolveProviderConfig reads it back correctly.
      const resolved = await resolveProviderConfig(projectRoot, 'openai');
      expect(resolved).toEqual({
        apiKey: 'fresh-write-key',
        baseUrl: 'https://fresh.test/v1',
      });
    });
  });
});
