import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode } from '../core/types';
import { graphAtom, graphOptions, currentGraphIdAtom } from '../core/graph-store';
import { addNodeToLocalGraphAtom } from '../core/graph-mutations';
import { viewportRectAtom } from '../core/viewport-store';
import {
	searchQueryAtom,
	setSearchQueryAtom,
	clearSearchAtom,
	searchResultsAtom,
	searchResultsArrayAtom,
	searchResultCountAtom,
	isFilterActiveAtom,
	highlightedSearchIndexAtom,
	nextSearchResultAtom,
	prevSearchResultAtom,
	highlightedSearchNodeIdAtom,
	fuzzyMatch,
} from '../core/search-store';

// --- Helpers ---

function freshStore() {
	const store = createStore();
	store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
	store.set(currentGraphIdAtom, 'test-graph');
	store.set(viewportRectAtom, { x: 0, y: 0, width: 800, height: 600, top: 0, left: 0, right: 800, bottom: 600, toJSON: () => ({}) } as DOMRect);
	return store;
}

function makeDbNode(id: string, label: string, nodeType = 'default'): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label,
		node_type: nodeType,
		configuration: null,
		ui_properties: { x: 0, y: 0, width: 200, height: 100, size: 15, zIndex: 0 },
		data: null,
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
	};
}

function addNode(store: ReturnType<typeof createStore>, id: string, label: string, nodeType = 'default') {
	store.set(addNodeToLocalGraphAtom, makeDbNode(id, label, nodeType));
}

// --- Tests ---

describe('search-store', () => {
	describe('search query', () => {
		it('starts with empty query', () => {
			const store = freshStore();
			expect(store.get(searchQueryAtom)).toBe('');
		});

		it('setSearchQueryAtom updates query and resets index', () => {
			const store = freshStore();
			store.set(highlightedSearchIndexAtom, 5);
			store.set(setSearchQueryAtom, 'hello');
			expect(store.get(searchQueryAtom)).toBe('hello');
			expect(store.get(highlightedSearchIndexAtom)).toBe(0);
		});

		it('clearSearchAtom clears query', () => {
			const store = freshStore();
			store.set(setSearchQueryAtom, 'test');
			store.set(clearSearchAtom);
			expect(store.get(searchQueryAtom)).toBe('');
		});
	});

	describe('search results', () => {
		it('returns empty set when no query', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Hello World');
			expect(store.get(searchResultsAtom).size).toBe(0);
		});

		it('matches by label (case-insensitive)', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Hello World');
			addNode(store, 'n2', 'Goodbye');
			addNode(store, 'n3', 'hello again');

			store.set(setSearchQueryAtom, 'hello');
			const results = store.get(searchResultsAtom);
			expect(results.size).toBe(2);
			expect(results.has('n1')).toBe(true);
			expect(results.has('n3')).toBe(true);
		});

		it('matches by node_type', () => {
			const store = freshStore();
			addNode(store, 'n1', 'A Node', 'widget');
			addNode(store, 'n2', 'B Node', 'data-source');

			store.set(setSearchQueryAtom, 'widget');
			const results = store.get(searchResultsAtom);
			expect(results.size).toBe(1);
			expect(results.has('n1')).toBe(true);
		});

		it('matches by node ID', () => {
			const store = freshStore();
			addNode(store, 'abc-123', 'Some Node');
			addNode(store, 'xyz-789', 'Other Node');

			store.set(setSearchQueryAtom, 'abc');
			const results = store.get(searchResultsAtom);
			expect(results.size).toBe(1);
			expect(results.has('abc-123')).toBe(true);
		});

		it('returns empty for no matches', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Hello');
			store.set(setSearchQueryAtom, 'zzzzz');
			expect(store.get(searchResultsAtom).size).toBe(0);
		});

		it('searchResultCountAtom returns correct count', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Test A');
			addNode(store, 'n2', 'Test B');
			addNode(store, 'n3', 'Other');

			store.set(setSearchQueryAtom, 'test');
			expect(store.get(searchResultCountAtom)).toBe(2);
		});
	});

	describe('filter active', () => {
		it('false when no query', () => {
			const store = freshStore();
			expect(store.get(isFilterActiveAtom)).toBe(false);
		});

		it('false for whitespace-only query', () => {
			const store = freshStore();
			store.set(setSearchQueryAtom, '   ');
			expect(store.get(isFilterActiveAtom)).toBe(false);
		});

		it('true when query is set', () => {
			const store = freshStore();
			store.set(setSearchQueryAtom, 'test');
			expect(store.get(isFilterActiveAtom)).toBe(true);
		});
	});

	describe('result navigation', () => {
		it('nextSearchResultAtom cycles through results', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Match A');
			addNode(store, 'n2', 'Match B');

			store.set(setSearchQueryAtom, 'match');
			expect(store.get(highlightedSearchIndexAtom)).toBe(0);

			store.set(nextSearchResultAtom);
			expect(store.get(highlightedSearchIndexAtom)).toBe(1);

			// Wraps around
			store.set(nextSearchResultAtom);
			expect(store.get(highlightedSearchIndexAtom)).toBe(0);
		});

		it('prevSearchResultAtom cycles backward', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Match A');
			addNode(store, 'n2', 'Match B');

			store.set(setSearchQueryAtom, 'match');
			// Start at 0, go backward → wraps to last
			store.set(prevSearchResultAtom);
			expect(store.get(highlightedSearchIndexAtom)).toBe(1);
		});

		it('highlightedSearchNodeIdAtom returns current node', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Match');

			store.set(setSearchQueryAtom, 'match');
			expect(store.get(highlightedSearchNodeIdAtom)).toBe('n1');
		});

		it('highlightedSearchNodeIdAtom returns null with no results', () => {
			const store = freshStore();
			expect(store.get(highlightedSearchNodeIdAtom)).toBeNull();
		});

		it('does nothing when no results', () => {
			const store = freshStore();
			store.set(nextSearchResultAtom);
			expect(store.get(highlightedSearchIndexAtom)).toBe(0);
		});
	});

	describe('fuzzy matching', () => {
		it('matches multi-token query in any order', () => {
			const store = freshStore();
			addNode(store, 'n1', 'Hello World Node');
			addNode(store, 'n2', 'Something Else');

			store.set(setSearchQueryAtom, 'node hello');
			const results = store.get(searchResultsAtom);
			expect(results.size).toBe(1);
			expect(results.has('n1')).toBe(true);
		});

		it('matches across label and node_type', () => {
			const store = freshStore();
			addNode(store, 'n1', 'My Widget', 'transformer');

			store.set(setSearchQueryAtom, 'widget trans');
			expect(store.get(searchResultsAtom).has('n1')).toBe(true);
		});
	});
});

describe('fuzzyMatch (pure function)', () => {
	it('single token matches substring', () => {
		expect(fuzzyMatch('inp', 'Input Node')).toBe(true);
	});

	it('multi-token all-must-match', () => {
		expect(fuzzyMatch('inp nod', 'Input Node')).toBe(true);
		expect(fuzzyMatch('inp xyz', 'Input Node')).toBe(false);
	});

	it('is case insensitive', () => {
		expect(fuzzyMatch('INPUT', 'input node')).toBe(true);
	});

	it('empty query returns false', () => {
		expect(fuzzyMatch('', 'anything')).toBe(false);
	});

	it('matches across multiple haystacks', () => {
		expect(fuzzyMatch('lab typ', 'My Label', 'custom-type')).toBe(true);
	});
});
