import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import {
  ProductManifestSchema,
  ExtendedProductManifestSchema,
  PlatformSchema,
  ThemeSchema,
  ContainerDefSchema,
  FeatureFlagSchema,
  BundleIdSchema,
  DEFAULT_THEME,
  loadProductManifest,
  loadProductManifestSync,
  resolveTheme,
  validateProductManifest,
  safeValidateProductManifest,
} from '../index.js';

// ── Minimal valid manifest ──────────────────────────────────────────────────

const MINIMAL = {
  productId: 'testprod',
  displayName: 'TestProd',
};

// ── Full manifest (matches FlowMonk-style) ──────────────────────────────────

const FULL = {
  productId: 'flowmonk',
  displayName: 'FlowMonk',
  name: 'FlowMonk',
  tagline: 'Agent-first planning and execution',
  domain: 'flowmonk.app',
  backendPort: 4017,
  primarySurface: 'web',
  mobileCompanion: true,
  platforms: ['web', 'ios', 'android'],
  bundleIds: {
    ios: 'com.saravana.flowmonk',
    android: 'com.saravana.flowmonk',
    web: 'flowmonk.app',
  },
  appStore: {
    category: 'Productivity',
    subcategory: 'Task Management',
    ageRating: '4+',
    privacyUrl: 'https://flowmonk.app/privacy',
    termsUrl: 'https://flowmonk.app/terms',
    supportUrl: 'https://flowmonk.app/support',
  },
  cosmos: {
    containers: [
      { name: 'zones', partitionKey: '/userId' },
      { name: 'flows', partitionKey: '/userId' },
      { name: 'tasks', partitionKey: '/userId' },
    ],
  },
  flags: [{ key: 'new-scheduler', defaultValue: false, description: 'Enable v2 scheduler' }],
  ports: { service: 4017 },
  version: '0.1.0',
};

// ── Tests ────────────────────────────────────────────────────────────────────

describe('ProductManifestSchema', () => {
  it('parses a minimal manifest (just productId + displayName)', () => {
    const result = ProductManifestSchema.parse(MINIMAL);
    expect(result.productId).toBe('testprod');
    expect(result.displayName).toBe('TestProd');
    expect(result.platforms).toEqual(['web']); // default
    expect(result.flags).toEqual([]); // default
  });

  it('parses a full manifest', () => {
    const result = ProductManifestSchema.parse(FULL);
    expect(result.productId).toBe('flowmonk');
    expect(result.backendPort).toBe(4017);
    expect(result.cosmos?.containers).toHaveLength(3);
    expect(result.flags).toHaveLength(1);
    expect(result.appStore?.category).toBe('Productivity');
  });

  it('rejects missing productId', () => {
    expect(() => ProductManifestSchema.parse({ displayName: 'X' })).toThrow();
  });

  it('rejects missing displayName', () => {
    expect(() => ProductManifestSchema.parse({ productId: 'x' })).toThrow();
  });

  it('rejects invalid productId (uppercase)', () => {
    expect(() => ProductManifestSchema.parse({ productId: 'BadId', displayName: 'X' })).toThrow(
      /lowercase/
    );
  });

  it('rejects productId starting with number', () => {
    expect(() => ProductManifestSchema.parse({ productId: '1bad', displayName: 'X' })).toThrow();
  });

  it('allows hyphens in productId', () => {
    const result = ProductManifestSchema.parse({ productId: 'my-app', displayName: 'My App' });
    expect(result.productId).toBe('my-app');
  });

  it('rejects backendPort below 1024', () => {
    expect(() => ProductManifestSchema.parse({ ...MINIMAL, backendPort: 80 })).toThrow();
  });

  it('rejects backendPort above 65535', () => {
    expect(() => ProductManifestSchema.parse({ ...MINIMAL, backendPort: 70000 })).toThrow();
  });

  it('accepts valid backendPort', () => {
    const result = ProductManifestSchema.parse({ ...MINIMAL, backendPort: 4016 });
    expect(result.backendPort).toBe(4016);
  });

  it('parses LysnrAI-style manifest (legacy fields)', () => {
    const lysnrai = {
      displayName: 'LysnrAI',
      productId: 'lysnrai',
      licensePrefix: 'LYSNR',
      configDirName: '.LysnrAI',
      envVarPrefix: 'LYSNR',
      bundleIdSuffix: 'LysnrAI',
      packageName: 'lysnrai',
    };
    const result = ProductManifestSchema.parse(lysnrai);
    expect(result.licensePrefix).toBe('LYSNR');
    expect(result.envVarPrefix).toBe('LYSNR');
  });

  it('parses NoteLett-style manifest (bundleId as object)', () => {
    const notelett = {
      productId: 'notelett',
      displayName: 'NoteLett',
      bundleId: { ios: 'com.bytelyst.notelett', android: 'com.notelett.app' },
      backendPort: 4016,
      appGroup: 'group.com.bytelyst.notelett',
    };
    const result = ProductManifestSchema.parse(notelett);
    expect(result.bundleId).toEqual({ ios: 'com.bytelyst.notelett', android: 'com.notelett.app' });
    expect(result.appGroup).toBe('group.com.bytelyst.notelett');
  });

  it('parses NomGap-style manifest (bundleId as string)', () => {
    const nomgap = {
      productId: 'nomgap',
      displayName: 'NomGap',
      bundleId: 'com.saravana.nomgap',
      domain: 'nomgap.app',
    };
    const result = ProductManifestSchema.parse(nomgap);
    expect(result.bundleId).toBe('com.saravana.nomgap');
  });

  it('parses ActionTrail-style manifest (no mobile)', () => {
    const actiontrail = {
      productId: 'actiontrail',
      displayName: 'ActionTrail',
      tagline: 'AI Activity Oversight',
      domain: 'actiontrail.dev',
      backendPort: 4018,
      primarySurface: 'web',
      mobileCompanion: false,
    };
    const result = ProductManifestSchema.parse(actiontrail);
    expect(result.mobileCompanion).toBe(false);
    expect(result.primarySurface).toBe('web');
  });
});

describe('duplicate container validation', () => {
  it('rejects duplicate container names', () => {
    const manifest = {
      ...MINIMAL,
      cosmos: {
        containers: [
          { name: 'users', partitionKey: '/userId' },
          { name: 'users', partitionKey: '/email' },
        ],
      },
    };
    expect(() => ProductManifestSchema.parse(manifest)).toThrow(/Duplicate container name: users/);
  });

  it('allows unique container names', () => {
    const manifest = {
      ...MINIMAL,
      cosmos: {
        containers: [
          { name: 'users', partitionKey: '/userId' },
          { name: 'sessions', partitionKey: '/userId' },
        ],
      },
    };
    const result = ProductManifestSchema.parse(manifest);
    expect(result.cosmos?.containers).toHaveLength(2);
  });

  it('allows single container (no duplicate check needed)', () => {
    const manifest = {
      ...MINIMAL,
      cosmos: { containers: [{ name: 'users', partitionKey: '/userId' }] },
    };
    const result = ProductManifestSchema.parse(manifest);
    expect(result.cosmos?.containers).toHaveLength(1);
  });
});

describe('ExtendedProductManifestSchema', () => {
  it('allows unknown keys (passthrough)', () => {
    const result = ExtendedProductManifestSchema.parse({
      ...MINIMAL,
      customField: 'hello',
      nestedCustom: { x: 1 },
    });
    expect((result as Record<string, unknown>).customField).toBe('hello');
  });
});

describe('PlatformSchema', () => {
  it('accepts valid platforms', () => {
    for (const p of ['web', 'ios', 'android', 'desktop', 'watch', 'mac']) {
      expect(PlatformSchema.parse(p)).toBe(p);
    }
  });

  it('rejects invalid platform', () => {
    expect(() => PlatformSchema.parse('windows')).toThrow();
  });
});

describe('ThemeSchema', () => {
  it('validates hex colors', () => {
    const result = ThemeSchema.parse(DEFAULT_THEME);
    expect(result.primary).toBe('#5AE68C');
  });

  it('rejects non-hex colors', () => {
    expect(() => ThemeSchema.parse({ ...DEFAULT_THEME, primary: 'red' })).toThrow(/hex/);
  });

  it('rejects 3-digit hex', () => {
    expect(() => ThemeSchema.parse({ ...DEFAULT_THEME, primary: '#FFF' })).toThrow();
  });
});

describe('ContainerDefSchema', () => {
  it('parses valid container', () => {
    const result = ContainerDefSchema.parse({ name: 'users', partitionKey: '/userId' });
    expect(result.name).toBe('users');
  });

  it('accepts optional ttlSeconds and uniqueKeys', () => {
    const result = ContainerDefSchema.parse({
      name: 'sessions',
      partitionKey: '/userId',
      ttlSeconds: 86400,
      uniqueKeys: ['/email'],
    });
    expect(result.ttlSeconds).toBe(86400);
    expect(result.uniqueKeys).toEqual(['/email']);
  });

  it('rejects empty name', () => {
    expect(() => ContainerDefSchema.parse({ name: '', partitionKey: '/x' })).toThrow();
  });

  it('rejects negative ttlSeconds', () => {
    expect(() =>
      ContainerDefSchema.parse({ name: 'x', partitionKey: '/x', ttlSeconds: -1 })
    ).toThrow();
  });
});

describe('FeatureFlagSchema', () => {
  it('accepts boolean default', () => {
    const result = FeatureFlagSchema.parse({ key: 'beta', defaultValue: false });
    expect(result.defaultValue).toBe(false);
  });

  it('accepts string default', () => {
    const result = FeatureFlagSchema.parse({ key: 'tier', defaultValue: 'free' });
    expect(result.defaultValue).toBe('free');
  });

  it('accepts number default', () => {
    const result = FeatureFlagSchema.parse({ key: 'max-items', defaultValue: 100 });
    expect(result.defaultValue).toBe(100);
  });

  it('rejects empty key', () => {
    expect(() => FeatureFlagSchema.parse({ key: '', defaultValue: true })).toThrow();
  });
});

describe('BundleIdSchema', () => {
  it('accepts string bundle ID', () => {
    expect(BundleIdSchema.parse('com.example.app')).toBe('com.example.app');
  });

  it('accepts per-platform bundle IDs', () => {
    const result = BundleIdSchema.parse({ ios: 'com.ios.app', android: 'com.android.app' });
    expect(result).toEqual({ ios: 'com.ios.app', android: 'com.android.app' });
  });

  it('rejects empty string', () => {
    expect(() => BundleIdSchema.parse('')).toThrow();
  });
});

describe('resolveTheme', () => {
  it('returns defaults when no theme specified', () => {
    const manifest = ProductManifestSchema.parse(MINIMAL);
    const theme = resolveTheme(manifest);
    expect(theme).toEqual(DEFAULT_THEME);
  });

  it('merges partial theme with defaults', () => {
    const manifest = ProductManifestSchema.parse({
      ...MINIMAL,
      theme: { primary: '#FF0000' },
    });
    const theme = resolveTheme(manifest);
    expect(theme.primary).toBe('#FF0000');
    expect(theme.secondary).toBe(DEFAULT_THEME.secondary); // default preserved
  });
});

describe('validateProductManifest', () => {
  it('validates a valid object', () => {
    const result = validateProductManifest(MINIMAL);
    expect(result.productId).toBe('testprod');
  });

  it('throws on invalid object', () => {
    expect(() => validateProductManifest({ bad: true })).toThrow();
  });
});

describe('safeValidateProductManifest', () => {
  it('returns manifest on valid input', () => {
    const result = safeValidateProductManifest(MINIMAL);
    expect(result).not.toBeNull();
    expect(result!.productId).toBe('testprod');
  });

  it('returns null on invalid input', () => {
    const result = safeValidateProductManifest({ bad: true });
    expect(result).toBeNull();
  });
});

describe('loadProductManifest / loadProductManifestSync', () => {
  const tmpDir = join(tmpdir(), `manifest-test-${Date.now()}`);
  const validPath = join(tmpDir, 'valid.json');
  const invalidPath = join(tmpDir, 'invalid.json');

  beforeEach(() => {
    mkdirSync(tmpDir, { recursive: true });
    writeFileSync(validPath, JSON.stringify(FULL));
    writeFileSync(invalidPath, JSON.stringify({ bad: true }));
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('loads and validates from file (async)', async () => {
    const result = await loadProductManifest(validPath);
    expect(result.productId).toBe('flowmonk');
    expect(result.cosmos?.containers).toHaveLength(3);
  });

  it('loads and validates from file (sync)', () => {
    const result = loadProductManifestSync(validPath);
    expect(result.productId).toBe('flowmonk');
  });

  it('throws on invalid file (async)', async () => {
    await expect(loadProductManifest(invalidPath)).rejects.toThrow();
  });

  it('throws on invalid file (sync)', () => {
    expect(() => loadProductManifestSync(invalidPath)).toThrow();
  });

  it('throws on missing file (async)', async () => {
    await expect(loadProductManifest('/nonexistent/path.json')).rejects.toThrow();
  });

  it('throws on missing file (sync)', () => {
    expect(() => loadProductManifestSync('/nonexistent/path.json')).toThrow();
  });
});

describe('real-world product.json files parse cleanly', () => {
  it('parses LysnrAI product.json', () => {
    const json = {
      displayName: 'LysnrAI',
      productId: 'lysnrai',
      licensePrefix: 'LYSNR',
      configDirName: '.LysnrAI',
      envVarPrefix: 'LYSNR',
      bundleIdSuffix: 'LysnrAI',
      packageName: 'lysnrai',
    };
    expect(() => ProductManifestSchema.parse(json)).not.toThrow();
  });

  it('parses NomGap product.json', () => {
    const json = {
      productId: 'nomgap',
      displayName: 'NomGap',
      bundleId: 'com.saravana.nomgap',
      domain: 'nomgap.app',
    };
    expect(() => ProductManifestSchema.parse(json)).not.toThrow();
  });

  it('parses NoteLett product.json', () => {
    const json = {
      productId: 'notelett',
      displayName: 'NoteLett',
      licensePrefix: 'NOTELETT',
      configDirName: '.NoteLett',
      envVarPrefix: 'NOTELETT',
      bundleIdSuffix: 'notelett',
      packageName: 'notelett',
      domain: 'notelett.app',
      bundleId: { ios: 'com.bytelyst.notelett', android: 'com.notelett.app' },
      appGroup: 'group.com.bytelyst.notelett',
      backendPort: 4016,
    };
    expect(() => ProductManifestSchema.parse(json)).not.toThrow();
  });

  it('parses FlowMonk product.json', () => {
    expect(() => ProductManifestSchema.parse(FULL)).not.toThrow();
  });

  it('parses ActionTrail product.json', () => {
    const json = {
      productId: 'actiontrail',
      displayName: 'ActionTrail',
      tagline: 'AI Activity Oversight',
      domain: 'actiontrail.dev',
      backendPort: 4018,
      primarySurface: 'web',
      mobileCompanion: false,
      bundleIds: { web: 'actiontrail.dev' },
      appStore: {
        category: 'Developer Tools',
        subcategory: 'AI Monitoring',
        privacyUrl: 'https://actiontrail.dev/privacy',
        termsUrl: 'https://actiontrail.dev/terms',
        supportUrl: 'https://actiontrail.dev/support',
      },
      version: '0.1.0',
    };
    expect(() => ProductManifestSchema.parse(json)).not.toThrow();
  });
});
