import { beforeEach, describe, expect, test } from 'vitest';
import { createDataset } from '@core/dataset';
import { createSnippet } from '@core/snippet';
import {
  selectCanSave,
  selectSelectedDataset,
  useDatasetStore,
  type DatasetForm,
} from './DatasetStore';
import { useSnippetStore } from './SnippetStore';

const store = () => useDatasetStore.getState();
const T = new Date('2026-06-01T00:00:00Z');

beforeEach(() => {
  store().reset();
  useSnippetStore.getState().reset();
});

describe('save — create', () => {
  test('valid inline JSON is added, profiled, and selected', () => {
    store().startCreate();
    store().updateForm({ name: 'Sales', input: '[{"a":1,"b":2},{"a":3,"b":4}]' });

    expect(store().save(T)).toBe(true);
    const ds = selectSelectedDataset(store());
    expect(ds?.name).toBe('Sales');
    expect(ds?.format).toBe('json');
    expect(ds?.rowCount).toBe(2);
    expect(ds?.columns).toEqual(['a', 'b']);
    expect(store().view).toBe('detail');
  });

  test('CSV input is stored as raw text and profiled', () => {
    store().startCreate();
    store().updateForm({ name: 'Regions', input: 'city,pop\nA,10\nB,20' });

    expect(store().save(T)).toBe(true);
    const ds = selectSelectedDataset(store());
    expect(ds?.format).toBe('csv');
    expect(ds?.data).toBe('city,pop\nA,10\nB,20');
    expect(ds?.rowCount).toBe(2);
  });

  test('save() does not commit a URL create — it must go through commitUrlSnapshot', () => {
    store().startCreate();
    store().updateForm({ name: 'Remote', source: 'url', input: 'https://example.com/data.csv' });
    // The component fetches first; save() never touches the network, so a URL create
    // through save() is a no-op rather than an unfetched record.
    expect(store().save(T)).toBe(false);
    expect(store().datasets).toHaveLength(0);
  });
});

describe('save — rename propagation (docs/architecture/07 §6)', () => {
  test('renaming via the edit form rewrites every referencing snippet', () => {
    store().startCreate();
    store().updateForm({ name: 'Sales', input: '[{"a":1}]' });
    expect(store().save(T)).toBe(true);

    // A published snippet referencing the dataset by name.
    useSnippetStore.getState().hydrate(
      [
        createSnippet({
          id: 'a',
          spec: JSON.stringify({ data: { name: 'Sales' }, mark: 'bar' }),
          now: T,
        }),
      ],
      'a',
    );
    useSnippetStore.getState().publish(T);
    expect(useSnippetStore.getState().snippets[0].datasetRefs).toEqual(['Sales']);

    store().startEdit();
    store().updateForm({ name: 'Revenue' });
    expect(store().save(new Date('2026-07-01T00:00:00Z'))).toBe(true);

    expect(store().datasets[0].name).toBe('Revenue');
    const s = useSnippetStore.getState().snippets[0];
    expect(s.datasetRefs).toEqual(['Revenue']);
    expect(s.spec).toContain('"Revenue"');
    expect(s.spec).not.toContain('"Sales"');
  });
});

describe('commitUrlSnapshot — create from a fetched body', () => {
  test('snapshots the body, infers format from content, and profiles it', () => {
    store().startCreate();
    store().updateForm({ name: 'Remote', source: 'url', input: 'https://example.com/data.csv' });

    expect(store().commitUrlSnapshot({ text: 'a,b\n1,2\n3,4' }, T)).toBe(true);
    const ds = selectSelectedDataset(store());
    expect(ds?.source).toBe('url');
    expect(ds?.url).toBe('https://example.com/data.csv');
    expect(ds?.format).toBe('csv');
    expect(ds?.data).toBe('a,b\n1,2\n3,4');
    expect(ds?.rowCount).toBe(2);
    expect(ds?.columns).toEqual(['a', 'b']);
    expect(ds?.fetchedAt).toBe(T.toISOString());
    expect(store().view).toBe('detail');
  });

  test('a duplicate name is rejected even with a successful fetch', () => {
    store().add(
      createDataset({ name: 'Dupe', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    store().startCreate();
    store().updateForm({ name: 'dupe', source: 'url', input: 'https://x/y.csv' });
    expect(store().commitUrlSnapshot({ text: 'a\n1' }, T)).toBe(false);
    expect(store().formError).toMatch(/already exists/i);
  });
});

describe('URL dataset edits & refresh', () => {
  const seedUrl = () => {
    store().startCreate();
    store().updateForm({ name: 'Remote', source: 'url', input: 'https://x/y.csv' });
    store().commitUrlSnapshot({ text: 'a,b\n1,2' }, T);
    return store().selectedId!;
  };

  test('a metadata-only edit (same URL) updates name without re-fetching', () => {
    const id = seedUrl();
    store().startEdit();
    store().updateForm({ name: 'Renamed' }); // URL field stays https://x/y.csv
    const later = new Date('2026-07-01T00:00:00Z');
    expect(store().save(later)).toBe(true);
    const ds = store().datasets.find((d) => d.id === id)!;
    expect(ds.name).toBe('Renamed');
    expect(ds.data).toBe('a,b\n1,2'); // snapshot untouched
    expect(ds.fetchedAt).toBe(T.toISOString()); // not re-fetched
    expect(ds.modified).toBe(later.toISOString());
  });

  test('refreshDataset re-snapshots, re-profiles, and advances fetchedAt', () => {
    const id = seedUrl();
    const later = new Date('2026-07-01T00:00:00Z');
    expect(store().refreshDataset(id, { text: 'a,b\n1,2\n3,4\n5,6' }, later)).toBe(true);
    const ds = store().datasets.find((d) => d.id === id)!;
    expect(ds.rowCount).toBe(3);
    expect(ds.fetchedAt).toBe(later.toISOString());
    expect(ds.modified).toBe(later.toISOString());
    expect(ds.name).toBe('Remote'); // preserved
  });

  test('a URL→inline conversion removes the url/fetchedAt keys (not left undefined)', () => {
    const id = seedUrl();
    store().startEdit();
    store().updateForm({ source: 'inline', input: '[{"x":1},{"x":2}]' });
    const later = new Date('2026-07-01T00:00:00Z');
    expect(store().save(later)).toBe(true);
    const ds = store().datasets.find((d) => d.id === id)!;
    expect(ds.source).toBe('inline');
    expect(ds.rowCount).toBe(2);
    // The remote origin is shed entirely — the keys are gone, not present-but-undefined,
    // so an inline record carries no stale url/fetchedAt.
    expect('url' in ds).toBe(false);
    expect('fetchedAt' in ds).toBe(false);
  });

  test('refreshDataset is a no-op for an inline dataset', () => {
    store().add(
      createDataset({ name: 'Inline', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const id = store().selectedId!;
    expect(store().refreshDataset(id, { text: 'a\n1' }, T)).toBe(false);
  });
});

describe('save — validation', () => {
  const submit = (patch: Partial<DatasetForm>) => {
    store().startCreate();
    store().updateForm(patch);
    return store().save(T);
  };

  test('blank name is rejected', () => {
    expect(submit({ name: '  ', input: '[{"a":1}]' })).toBe(false);
    expect(store().formError).toMatch(/name/i);
    expect(store().datasets).toHaveLength(0);
  });

  test('duplicate name (case-insensitive) is rejected', () => {
    store().add(
      createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    expect(submit({ name: 'sales', input: '[{"a":1}]' })).toBe(false);
    expect(store().formError).toMatch(/already exists/i);
  });

  test('empty and unrecognized inline data are rejected', () => {
    expect(submit({ name: 'Empty', input: '   ' })).toBe(false);
    expect(submit({ name: 'Junk', input: 'this is not data' })).toBe(false);
    expect(store().formError).toMatch(/valid JSON/i);
  });

  test('a non-http URL is rejected', () => {
    expect(submit({ name: 'Bad', source: 'url', input: 'ftp://x/y.csv' })).toBe(false);
    expect(store().formError).toMatch(/url/i);
  });
});

describe('selectCanSave — drives the Save button disabled state (§05)', () => {
  const canSaveWith = (patch: Partial<DatasetForm>) => {
    store().startCreate();
    store().updateForm(patch);
    return selectCanSave(useDatasetStore.getState());
  };

  test('false until a name and valid data are present', () => {
    store().startCreate();
    expect(selectCanSave(useDatasetStore.getState())).toBe(false); // empty form
    expect(canSaveWith({ name: 'Sales' })).toBe(false); // no data
    expect(canSaveWith({ name: '  ', input: '[{"a":1}]' })).toBe(false); // blank name
    expect(canSaveWith({ name: 'Sales', input: 'not data' })).toBe(false); // unrecognized
  });

  test('true for valid inline data and valid URLs', () => {
    expect(canSaveWith({ name: 'Sales', input: '[{"a":1,"b":2}]' })).toBe(true);
    expect(canSaveWith({ name: 'Regions', input: 'city,pop\nA,10' })).toBe(true);
    expect(canSaveWith({ name: 'Remote', source: 'url', input: 'https://x/y.csv' })).toBe(true);
  });

  test('false for a non-http URL', () => {
    expect(canSaveWith({ name: 'Bad', source: 'url', input: 'ftp://x/y.csv' })).toBe(false);
  });

  test('false on a duplicate name, true once the editing dataset keeps its own name', () => {
    store().add(
      createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const id = store().datasets[0].id;
    // Creating another "Sales" is blocked.
    expect(canSaveWith({ name: 'Sales', input: '[{"a":1}]' })).toBe(false);
    // Editing the existing one may keep its own name (excludeId).
    store().select(id);
    store().startEdit();
    expect(selectCanSave(useDatasetStore.getState())).toBe(true);
  });
});

describe('save — edit', () => {
  test('updating inline data re-profiles and advances modified', () => {
    store().add(
      createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const id = store().selectedId!;
    store().startEdit();
    store().updateForm({ input: '[{"a":1,"b":2},{"a":3,"b":4}]' });

    const later = new Date('2026-07-01T00:00:00Z');
    expect(store().save(later)).toBe(true);
    const ds = store().datasets.find((d) => d.id === id)!;
    expect(ds.columnCount).toBe(2);
    expect(ds.modified).toBe(later.toISOString());
  });

  test('renaming a referenced dataset propagates into referencing snippets', () => {
    store().add(
      createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const snippet = createSnippet({
      id: 's1',
      spec: JSON.stringify({ data: { name: 'Sales' }, mark: 'bar' }),
      now: T,
    });
    useSnippetStore.getState().hydrate([snippet], 's1');
    useSnippetStore.getState().publish(T); // seed datasetRefs = ['Sales']

    store().startEdit();
    store().updateForm({ name: 'Revenue' });
    expect(store().save(new Date('2026-08-01T00:00:00Z'))).toBe(true);

    expect(store().datasets[0].name).toBe('Revenue');
    const s = useSnippetStore.getState().snippets.find((x) => x.id === 's1')!;
    expect(s.datasetRefs).toEqual(['Revenue']);
    expect(s.spec).toContain('"Revenue"');
  });
});

describe('remove & view transitions', () => {
  test('removing the selected dataset clears the selection and returns to the list', () => {
    store().add(
      createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const id = store().selectedId!;
    store().remove(id);
    expect(store().selectedId).toBeNull();
    expect(store().view).toBe('list');
  });

  test('cancelForm returns to detail when a dataset is selected', () => {
    store().add(
      createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    store().startEdit();
    expect(store().view).toBe('edit');
    store().cancelForm();
    expect(store().view).toBe('detail');
  });
});

describe('id assignment — the store is the collision-free id authority', () => {
  test('add reassigns a fresh id even when two creates share a Date.now() id', () => {
    // Simulate the tight-loop hazard: two datasets minted with the SAME injected id
    // (what Date.now() yields within one millisecond). add must still give them
    // distinct, monotonically increasing ids so they cannot collide in IndexedDB.
    store().add(
      createDataset({
        name: 'A',
        data: [{ a: 1 }],
        format: 'json',
        source: 'inline',
        now: T,
        id: 42,
      }),
    );
    store().add(
      createDataset({
        name: 'B',
        data: [{ a: 1 }],
        format: 'json',
        source: 'inline',
        now: T,
        id: 42,
      }),
    );
    const ids = store().datasets.map((d) => d.id);
    expect(new Set(ids).size).toBe(2);
    // selectedId tracks the reassigned id of the most recent add, not the input 42.
    expect(store().selectedId).toBe(store().datasets[0].id);
  });

  test('addDatasets gives a batch distinct ids past the existing maximum', () => {
    store().add(
      createDataset({ name: 'Seed', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
    );
    const seedId = store().datasets[0].id;
    store().addDatasets([
      createDataset({
        name: 'X',
        data: [{ a: 1 }],
        format: 'json',
        source: 'inline',
        now: T,
        id: 42,
      }),
      createDataset({
        name: 'Y',
        data: [{ a: 1 }],
        format: 'json',
        source: 'inline',
        now: T,
        id: 42,
      }),
    ]);
    const ids = store().datasets.map((d) => d.id);
    expect(new Set(ids).size).toBe(3);
    expect(Math.min(...ids.filter((i) => i !== seedId))).toBeGreaterThan(seedId);
  });
});
