import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { acceptSnapshot, DEFAULT_FRESHNESS_POLICY } from './model';
import { clearSnapshotCache, readSnapshotCache, writeSnapshotCache } from './snapshot-cache';
import { validateProjectCollection, validateTaskCollection } from './validators';
import { projectFixtures, taskFixtures } from '@/spa/pages/page-fixtures';
import type { Project, Task } from '@/lib/types';

const KEY = 'test:tasks';
const NOW = 1_800_000_000_000;
const policy = { ...DEFAULT_FRESHNESS_POLICY, staleAfterMs: 60_000 };

function storedTaskSnapshot() {
  const result = acceptSnapshot({
    value: taskFixtures,
    validate: validateTaskCollection,
    previous: null,
    policy,
    source: 'gateway:/api/tasks',
    now: NOW,
  });
  if (result.outcome !== 'accepted') throw new Error('fixture setup failed');
  return result.snapshot;
}

function storedProjectSnapshot() {
  const result = acceptSnapshot({
    value: projectFixtures,
    validate: validateProjectCollection,
    previous: null,
    policy,
    source: 'gateway:/api/projects',
    now: NOW,
  });
  if (result.outcome !== 'accepted') throw new Error('fixture setup failed');
  return result.snapshot;
}

function readTasks() {
  return readSnapshotCache({
    key: KEY,
    workspace: policy.workspace,
    policy,
    validate: validateTaskCollection,
  });
}

/** Write an arbitrary value directly at the raw cache slot. */
function writeRaw(key: string, value: unknown): void {
  sessionStorage.setItem(`mosaic:freshness:v1:${key}`, JSON.stringify(value));
}

/** Parse and re-write the stored entry (for tampering with internals). */
function tamperStored<T>(key: string, mutate: (stored: T) => void): void {
  const parsed = JSON.parse(sessionStorage.getItem(`mosaic:freshness:v1:${key}`) ?? '{}') as T;
  mutate(parsed);
  writeRaw(key, parsed);
}

beforeEach(() => {
  sessionStorage.clear();
});

afterEach(() => {
  sessionStorage.clear();
});

describe('readSnapshotCache', () => {
  it('misses when nothing is stored', () => {
    expect(readTasks()).toEqual({ outcome: 'miss' });
  });

  it('hits for a well-formed entry and preserves provenance', () => {
    const snapshot = storedTaskSnapshot();
    writeSnapshotCache(KEY, snapshot);

    const result = readTasks();
    expect(result.outcome).toBe('hit');
    if (result.outcome === 'hit') {
      expect(result.snapshot.data).toEqual(taskFixtures);
      expect(result.snapshot.source).toBe('gateway:/api/tasks');
      expect(result.snapshot.version).toBe(snapshot.version);
      expect(result.snapshot.fetchedAt).toBe(snapshot.fetchedAt);
      expect(result.snapshot.workspace).toBe(snapshot.workspace);
    }
  });

  it('invalidates unparsable entries as cache corruption', () => {
    sessionStorage.setItem(`mosaic:freshness:v1:${KEY}`, '{not json');
    expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'cache-corruption' });
  });

  it('invalidates structurally wrong entries as cache corruption', () => {
    const malformed: unknown[] = [
      'nested but not a snapshot',
      { data: taskFixtures }, // missing provenance fields
      {
        data: taskFixtures,
        source: 1,
        workspace: 'w',
        version: 1,
        schemaVersion: 1,
        fetchedAt: 1,
        digest: 'x',
      },
      null,
      17,
    ];
    for (const entry of malformed) {
      writeRaw(KEY, entry);
      expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'cache-corruption' });
    }
  });

  it('invalidates digest mismatches as cache corruption (tampered data)', () => {
    writeSnapshotCache(KEY, storedTaskSnapshot());
    tamperStored<{ data: Task[] }>(KEY, (stored) => {
      stored.data = [...stored.data, { ...stored.data[0]!, id: 'injected-task' }];
    });
    expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'cache-corruption' });
  });

  it('invalidates entries scoped to another workspace', () => {
    const snapshot = storedTaskSnapshot();
    writeSnapshotCache(KEY, { ...snapshot, workspace: 'someone-else' });
    expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'cross-workspace' });
  });

  it('invalidates entries written by a newer schema as a version regression', () => {
    const snapshot = storedTaskSnapshot();
    writeSnapshotCache(KEY, { ...snapshot, schemaVersion: policy.schemaVersion + 1 });
    expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'version-regression' });
  });

  it('invalidates entries whose data no longer validates (schema mismatch)', () => {
    writeSnapshotCache(KEY, storedTaskSnapshot());
    tamperStored<{ data: unknown }>(KEY, (stored) => {
      stored.data = { malformed: true };
    });
    expect(readTasks()).toEqual({ outcome: 'invalidated', reason: 'schema-mismatch' });
  });

  it('never reports a corrupted raw entry as a hit (negative control)', () => {
    for (const raw of ['{oops', 'null', '"string"', '[]', '12']) {
      sessionStorage.setItem(`mosaic:freshness:v1:${KEY}`, raw);
      const result = readTasks();
      expect(result.outcome).not.toBe('hit');
      expect(result.outcome).toBe('invalidated');
    }
  });

  it('scopes project collections by their workspace identity', () => {
    const snapshot = storedProjectSnapshot();
    writeSnapshotCache('test:projects', snapshot);

    const sameScope = readSnapshotCache({
      key: 'test:projects',
      workspace: 'user-1',
      policy,
      validate: validateProjectCollection,
    });
    expect(sameScope.outcome).toBe('hit');

    const foreignScope = readSnapshotCache({
      key: 'test:projects',
      workspace: 'user-2',
      policy,
      validate: validateProjectCollection,
    });
    expect(foreignScope).toEqual({ outcome: 'invalidated', reason: 'cross-workspace' });
  });
});

describe('writeSnapshotCache round-trip', () => {
  it('round-trips an accepted project snapshot', () => {
    const snapshot = storedProjectSnapshot();
    writeSnapshotCache('test:projects', snapshot);
    const result = readSnapshotCache({
      key: 'test:projects',
      workspace: snapshot.workspace,
      policy,
      validate: validateProjectCollection,
    });
    expect(result.outcome).toBe('hit');
    if (result.outcome === 'hit') {
      expect(result.snapshot.data).toEqual(projectFixtures as Project[]);
    }
  });
});

describe('clearSnapshotCache', () => {
  it('drops the entry so the next read misses', () => {
    writeSnapshotCache(KEY, storedTaskSnapshot());
    expect(readTasks().outcome).toBe('hit');
    clearSnapshotCache(KEY);
    expect(readTasks()).toEqual({ outcome: 'miss' });
  });
});
