import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defaultSettings } from '@core/settings';
import {
  loadChartTheme,
  loadPreviewFitMode,
  loadUiTheme,
  loadUserSettings,
  saveChartTheme,
  saveManagedSettings,
  savePreviewFitMode,
  saveUiTheme,
} from './settings-store';

const KEY = 'astrolabe:settings';

/**
 * In-memory localStorage stub. The adapter is tested against a stub rather than
 * the ambient global (docs/architecture/02 §5) — doubly necessary here because
 * Node ships a non-functional `localStorage` global that shadows happy-dom's.
 */
function makeStorageStub() {
  const map = new Map<string, string>();
  return {
    getItem: (k: string) => (map.has(k) ? map.get(k)! : null),
    setItem: (k: string, v: string) => void map.set(k, String(v)),
    removeItem: (k: string) => void map.delete(k),
    clear: () => map.clear(),
    key: (i: number) => [...map.keys()][i] ?? null,
    get length() {
      return map.size;
    },
  };
}

describe('settings-store · ui.theme', () => {
  beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
  afterEach(() => vi.unstubAllGlobals());

  describe('loadUiTheme', () => {
    it('defaults to light when nothing is stored', () => {
      expect(loadUiTheme()).toBe('light');
    });

    it('returns a stored valid theme', () => {
      localStorage.setItem(KEY, JSON.stringify({ ui: { theme: 'dark' } }));
      expect(loadUiTheme()).toBe('dark');
    });

    it('falls back to light on malformed JSON', () => {
      localStorage.setItem(KEY, '{ not valid json');
      expect(loadUiTheme()).toBe('light');
    });

    it('falls back for an unrecognized/legacy value (e.g. retired "experimental")', () => {
      localStorage.setItem(KEY, JSON.stringify({ ui: { theme: 'experimental' } }));
      expect(loadUiTheme()).toBe('light');
    });

    it('tolerates a record with no ui group', () => {
      localStorage.setItem(KEY, JSON.stringify({ formatting: { dateFormat: 'iso' } }));
      expect(loadUiTheme()).toBe('light');
    });
  });

  describe('saveUiTheme', () => {
    it('round-trips through load', () => {
      saveUiTheme('dark');
      expect(loadUiTheme()).toBe('dark');
    });

    it('preserves other keys already in the settings record (forward-compatible merge)', () => {
      // Simulate a future/full UserSettings record written by M5.
      localStorage.setItem(
        KEY,
        JSON.stringify({
          version: 1,
          editor: { fontSize: 14 },
          ui: { theme: 'light', previewFitMode: 'width' },
        }),
      );

      saveUiTheme('dark');

      const stored = JSON.parse(localStorage.getItem(KEY)!) as {
        version: number;
        ui: { theme: string; previewFitMode: string };
        editor: { fontSize: number };
      };
      expect(stored.ui.theme).toBe('dark');
      // Everything else survives — nothing clobbered.
      expect(stored.version).toBe(1);
      expect(stored.editor.fontSize).toBe(14);
      expect(stored.ui.previewFitMode).toBe('width');
    });
  });
});

describe('settings-store · ui.previewFitMode', () => {
  beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
  afterEach(() => vi.unstubAllGlobals());

  it('defaults to Original (default) when nothing is stored', () => {
    expect(loadPreviewFitMode()).toBe('default');
  });

  it('returns a stored valid fit mode', () => {
    localStorage.setItem(KEY, JSON.stringify({ ui: { previewFitMode: 'full' } }));
    expect(loadPreviewFitMode()).toBe('full');
  });

  it('falls back to default for an unrecognized value', () => {
    localStorage.setItem(KEY, JSON.stringify({ ui: { previewFitMode: 'cover' } }));
    expect(loadPreviewFitMode()).toBe('default');
  });

  it('round-trips through load and preserves the theme slice', () => {
    saveUiTheme('dark');
    savePreviewFitMode('height');
    expect(loadPreviewFitMode()).toBe('height');
    expect(loadUiTheme()).toBe('dark'); // the other slice survives the merge
  });
});

describe('settings-store · ui.chartTheme', () => {
  beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
  afterEach(() => vi.unstubAllGlobals());

  it('defaults to astrolabe when nothing is stored', () => {
    expect(loadChartTheme()).toBe('astrolabe');
  });

  it('returns a stored valid chart theme', () => {
    localStorage.setItem(KEY, JSON.stringify({ ui: { chartTheme: 'stock' } }));
    expect(loadChartTheme()).toBe('stock');
  });

  it('falls back to astrolabe for an unrecognized id', () => {
    localStorage.setItem(KEY, JSON.stringify({ ui: { chartTheme: 'neon' } }));
    expect(loadChartTheme()).toBe('astrolabe');
  });

  it('round-trips and preserves the other ui slices', () => {
    saveUiTheme('dark');
    savePreviewFitMode('height');
    saveChartTheme('powerbi');
    expect(loadChartTheme()).toBe('powerbi');
    expect(loadUiTheme()).toBe('dark');
    expect(loadPreviewFitMode()).toBe('height');
  });
});

describe('settings-store · full UserSettings record', () => {
  beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
  afterEach(() => vi.unstubAllGlobals());

  it('loadUserSettings returns defaults when nothing is stored', () => {
    expect(loadUserSettings()).toEqual(defaultSettings());
  });

  it('loadUserSettings normalizes a partial stored record', () => {
    localStorage.setItem(KEY, JSON.stringify({ editor: { fontSize: 99 } }));
    const loaded = loadUserSettings();
    expect(loaded.editor.fontSize).toBe(18); // clamped to the 10–18 range
    expect(loaded.performance.renderDebounce).toBe(1500); // gap filled from defaults
  });

  it('saveManagedSettings round-trips and leaves the ui slice untouched', () => {
    saveUiTheme('dark');
    savePreviewFitMode('width');

    const managed = defaultSettings();
    managed.editor.fontSize = 16;
    managed.performance.renderDebounce = 800;
    managed.formatting.dateFormat = 'iso';
    saveManagedSettings(managed);

    const loaded = loadUserSettings();
    expect(loaded.editor.fontSize).toBe(16);
    expect(loaded.performance.renderDebounce).toBe(800);
    expect(loaded.formatting.dateFormat).toBe('iso');
    // The ui slice (written by the narrow slice writers) survives the managed merge.
    expect(loaded.ui.theme).toBe('dark');
    expect(loaded.ui.previewFitMode).toBe('width');
  });
});
