import { mockReactFlow } from '../../../../setupFlowTests.ts';
import { act, render, screen, fireEvent } from '@testing-library/react';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
import { addNode } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx';
import { makeProjectBlob } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx';
import SaveLoadPanel from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx';

beforeAll(() => {
  mockReactFlow();
});

beforeEach(() => {
  const { setNodes, setEdges } = useFlowStore.getState();
  act(() => {
    setNodes([]);
    setEdges([]);
  });
});

afterEach(() => {
  jest.restoreAllMocks();
});

describe('Load and save panel', () => {
  test('save and load functions work correctly', async () => {
    // create two nodes via your sidebar API
    act(() => {
      addNode('phase', { x: 100, y: 100 });
      addNode('norm',  { x: 200, y: 200 });
    });

    const initialState = useFlowStore.getState();
    expect(initialState.nodes.length).toBe(2);

    // make blob from current nodes/edges
    const blob = makeProjectBlob('test-project', initialState.nodes, initialState.edges);

    // simulate loading from that blob
    const parsed = JSON.parse(await blobToText(blob));

    act(() => {
      const { setNodes, setEdges } = useFlowStore.getState();
      setEdges([]); // clear edges first (mirrors app behavior)
      setNodes(parsed.nodes);
      setEdges(parsed.edges);
    });

    const loadedState = useFlowStore.getState();
    expect(loadedState.nodes.length).toBe(2);
    expect(loadedState.nodes).toEqual(initialState.nodes);
    expect(loadedState.edges).toEqual(initialState.edges);
  });

  test('Save uses showSaveFilePicker and writes JSON', async () => {
    // Seed a simple graph so Save has something to write
    act(() => {
      useFlowStore.getState().setNodes([
        { id: 'start', type: 'start', position: { x: 0, y: 0 }, data: { label: 'start' } } as any,
        { id: 'phase-1', type: 'phase', position: { x: 100, y: 120 }, data: { label: 'P1', number: 1 } } as any,
        { id: 'end', type: 'end', position: { x: 0, y: 300 }, data: { label: 'End' } } as any,
      ]);
      useFlowStore.getState().setEdges([
        { id: 'start-phase-1', source: 'start', target: 'phase-1' } as any,
      ]);
    });

    // capture what the app writes; don't decode inside the spy
    let writtenChunk: any = null;
    const write = jest.fn(async (chunk: any) => { writtenChunk = chunk; });
    const close = jest.fn().mockResolvedValue(undefined);
    const createWritable = jest.fn().mockResolvedValue({ write, close });

    // Mock the picker
    (window as any).showSaveFilePicker = jest.fn().mockResolvedValue({ createWritable });

    render(<SaveLoadPanel />);

    await act(async () => {
      fireEvent.click(screen.getByText(/Save Graph/i));
    });
    // @ts-expect-error
    expect(window.showSaveFilePicker).toHaveBeenCalledTimes(1);
    expect(createWritable).toHaveBeenCalledTimes(1);
    expect(write).toHaveBeenCalledTimes(1);
    expect(close).toHaveBeenCalledTimes(1);

    const writtenText = await chunkToString(writtenChunk);
    const json = JSON.parse(writtenText);
    expect(json.version).toBe(1);
    expect(json.name).toBeDefined();
    expect(Array.isArray(json.nodes)).toBe(true);
    expect(Array.isArray(json.edges)).toBe(true);
    expect(json.behaviorProgram).toBeUndefined();
  });

  test('Save falls back to anchor download when picker unavailable', async () => {
    // Remove picker so we hit the fallback
    delete (window as any).showSaveFilePicker;

    // Keep a reference to the REAL createElement to avoid recursion
    const realCreateElement = document.createElement.bind(document);

    // Spy on URL + anchor click
    const origCreateObjectURL = URL.createObjectURL;
    const origRevokeObjectURL = URL.revokeObjectURL;
    (URL as any).createObjectURL = jest.fn(() => 'blob:fake-url');
    (URL as any).revokeObjectURL = jest.fn();

    const clickSpy = jest.fn();
    const createElementSpy = jest
      .spyOn(document, 'createElement')
      .mockImplementation((tag: any, opts?: any) => {
        if (tag === 'a') {
          // return a minimal anchor with a click spy
          return {
            set href(_v: string) {},
            set download(_v: string) {},
            click: clickSpy,
          } as unknown as HTMLAnchorElement;
        }
        // call the REAL createElement for everything else
        return realCreateElement(tag, opts as any);
      });

    render(<SaveLoadPanel />);

    await act(async () => {
      fireEvent.click(screen.getByText(/Save Graph/i));
    });

    expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
    expect(clickSpy).toHaveBeenCalledTimes(1);

    // cleanup
    createElementSpy.mockRestore();
    (URL as any).createObjectURL = origCreateObjectURL;
    (URL as any).revokeObjectURL = origRevokeObjectURL;
  });
});

//
// helpers
//

// portable blob reader (no Response needed)
async function blobToText(blob: Blob): Promise<string> {
  const anyBlob = blob as any;
  if (typeof anyBlob.text === 'function') return anyBlob.text();
  if (typeof anyBlob.arrayBuffer === 'function') {
    const buf = await anyBlob.arrayBuffer();
    return new TextDecoder().decode(buf);
  }
  return await new Promise<string>((resolve, reject) => {
    const fr = new FileReader();
    fr.onload = () => resolve(String(fr.result));
    fr.onerror = () => reject(fr.error);
    fr.readAsText(blob);
  });
}

// normalize whatever chunk createWritable.write receives to a string
async function chunkToString(chunk: any): Promise<string> {
  if (typeof chunk === 'string') return chunk;
  if (chunk instanceof Blob) return blobToText(chunk);
  if (chunk?.buffer instanceof ArrayBuffer) {
    return new TextDecoder().decode(chunk as Uint8Array);
  }
  if (chunk instanceof ArrayBuffer) {
    return new TextDecoder().decode(new Uint8Array(chunk));
  }
  return String(chunk);
}
