import { describe, it, expect } from 'vitest';
import { buildStackDossierMarkdown, EMPTY_DOSSIER_FIELDS, type StackDossierFields } from './dossierMarkdown';
import type { AnatomyMarkdownInput } from './anatomyMarkdown';

const anatomy: AnatomyMarkdownInput = {
  stackName: 'plex',
  services: ['plex'],
  ports: { plex: [{ host: '32400', container: '32400', proto: 'tcp' }] },
  volumes: {},
  restart: 'unless-stopped',
  envFile: '.env',
  envVarCount: 4,
  missingVars: ['CLAIM_TOKEN'],
  networkName: 'plex_default',
  gitSource: null,
};

const fields = (over: Partial<StackDossierFields> = {}): StackDossierFields => ({ ...EMPTY_DOSSIER_FIELDS, ...over });

describe('buildStackDossierMarkdown', () => {
  it('returns only the anatomy markdown when no operator notes are set', () => {
    const md = buildStackDossierMarkdown(anatomy, fields());
    expect(md).toContain('# plex');
    expect(md).toContain('## Services');
    expect(md).not.toContain('## Operator notes');
    expect(md).not.toContain('## Network exposure');
  });

  it('appends the network exposure section when a summary is provided', () => {
    const md = buildStackDossierMarkdown(anatomy, fields(), {
      stackIntent: 'internal',
      networks: [{ name: 'plex_default', external: false, internal: false }],
      services: [{ name: 'plex', intent: null, ports: ['32400/tcp (all interfaces)'], hostNetwork: false }],
    });
    expect(md).toContain('## Network exposure');
    expect(md).toContain('**Stack intent:** internal');
    expect(md).toContain('32400/tcp (all interfaces)');
  });

  it('appends an Operator notes section with the filled fields', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({
      purpose: 'Media server',
      owner: 'home',
      static_ip: '10.0.10.4',
      backup_notes: 'rsync config nightly',
    }));
    expect(md).toContain('## Operator notes');
    expect(md).toContain('- **Purpose:** Media server');
    expect(md).toContain('- **Owner:** home');
    expect(md).toContain('- **Static IP:** 10.0.10.4');
    expect(md).toContain('### Backup\nrsync config nightly');
  });

  it('omits empty operator fields', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'only this' }));
    expect(md).toContain('- **Purpose:** only this');
    expect(md).not.toContain('- **Owner:**');
    expect(md).not.toContain('### Firewall');
  });

  it('keeps the generated anatomy facts in the combined export', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'p' }));
    expect(md).toContain('| plex | 32400 | 32400 | tcp |');
    expect(md).toContain('- Variables: 4');
    expect(md).toContain('- Missing: `CLAIM_TOKEN`');
  });

  it('preserves multi-line access URLs as a block', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ access_urls: 'https://a.example\nhttps://b.example' }));
    expect(md).toContain('### Access URLs\nhttps://a.example\nhttps://b.example');
  });

  it('collapses stray newlines in a single-line field into one bullet', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'line one\nline two' }));
    expect(md).toContain('- **Purpose:** line one line two');
  });

  it('the generated facts never emit a .env assignment, only variable names and counts', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ custom_notes: 'see runbook' }));
    expect(md).toContain('- Variables: 4');
    expect(md).toContain('- Missing: `CLAIM_TOKEN`');
    expect(md).not.toMatch(/CLAIM_TOKEN\s*=/);
  });

  it('exports operator notes verbatim (user-authored content is not redacted)', () => {
    const md = buildStackDossierMarkdown(anatomy, fields({ custom_notes: 'DB_PASSWORD=hunter2' }));
    expect(md).toContain('DB_PASSWORD=hunter2');
  });

  it('is deterministic across distinct but equal inputs', () => {
    const f = fields({ purpose: 'x', firewall_notes: 'y' });
    expect(buildStackDossierMarkdown(anatomy, f)).toBe(buildStackDossierMarkdown(anatomy, { ...f }));
  });
});
