/**
 * P0 Unit Tests: Knowledge Graph
 *
 * Tests: createKnowledgeGraph() — addNode, getNode, removeNode,
 * iterNodes, addRelationship, removeNodesByFile, counts.
 */
import { describe, it, expect } from 'vitest';
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import type { GraphNode, GraphRelationship } from '../../src/core/graph/types.js';

function makeNode(id: string, name: string, filePath: string = 'src/test.ts'): GraphNode {
  return {
    id,
    label: 'Function',
    properties: { name, filePath, startLine: 1, endLine: 10 },
  };
}

function makeRel(
  src: string,
  tgt: string,
  type: GraphRelationship['type'] = 'CALLS',
): GraphRelationship {
  return {
    id: `${src}-${type}-${tgt}`,
    sourceId: src,
    targetId: tgt,
    type,
    confidence: 1.0,
    reason: '',
  };
}

describe('createKnowledgeGraph', () => {
  // ─── addNode / getNode ─────────────────────────────────────────────

  it('adds and retrieves a node', () => {
    const g = createKnowledgeGraph();
    const node = makeNode('fn:foo', 'foo');
    g.addNode(node);
    expect(g.getNode('fn:foo')).toBe(node);
  });

  it('returns undefined for unknown node', () => {
    const g = createKnowledgeGraph();
    expect(g.getNode('nonexistent')).toBeUndefined();
  });

  it('duplicate addNode is a no-op', () => {
    const g = createKnowledgeGraph();
    const node1 = makeNode('fn:foo', 'foo');
    const node2 = makeNode('fn:foo', 'bar'); // same ID, different name
    g.addNode(node1);
    g.addNode(node2);
    expect(g.nodeCount).toBe(1);
    expect(g.getNode('fn:foo')!.properties.name).toBe('foo'); // first one wins
  });

  // ─── removeNode ─────────────────────────────────────────────────────

  it('removes a node and its relationships', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    expect(g.relationshipCount).toBe(1);

    const removed = g.removeNode('fn:a');
    expect(removed).toBe(true);
    expect(g.getNode('fn:a')).toBeUndefined();
    expect(g.nodeCount).toBe(1);
    expect(g.relationshipCount).toBe(0); // relationship involving fn:a removed
  });

  it('removeNode returns false for unknown node', () => {
    const g = createKnowledgeGraph();
    expect(g.removeNode('nope')).toBe(false);
  });

  // ─── removeNodesByFile ──────────────────────────────────────────────

  it('removes all nodes belonging to a file', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a', 'src/foo.ts'));
    g.addNode(makeNode('fn:b', 'b', 'src/foo.ts'));
    g.addNode(makeNode('fn:c', 'c', 'src/bar.ts'));

    const removed = g.removeNodesByFile('src/foo.ts');
    expect(removed).toBe(2);
    expect(g.nodeCount).toBe(1);
    expect(g.getNode('fn:c')).toBeDefined();
  });

  // ─── iterNodes / iterRelationships ─────────────────────────────────

  it('iterNodes yields all nodes', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));

    const ids = [...g.iterNodes()].map((n) => n.id);
    expect(ids).toHaveLength(2);
    expect(ids).toContain('fn:a');
    expect(ids).toContain('fn:b');
  });

  it('iterRelationships yields all relationships', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));

    const rels = [...g.iterRelationships()];
    expect(rels).toHaveLength(1);
    expect(rels[0].sourceId).toBe('fn:a');
  });

  // ─── nodeCount / relationshipCount ─────────────────────────────────

  it('nodeCount reflects current node count', () => {
    const g = createKnowledgeGraph();
    expect(g.nodeCount).toBe(0);
    g.addNode(makeNode('fn:a', 'a'));
    expect(g.nodeCount).toBe(1);
    g.addNode(makeNode('fn:b', 'b'));
    expect(g.nodeCount).toBe(2);
  });

  it('relationshipCount reflects current relationship count', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    expect(g.relationshipCount).toBe(0);
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    expect(g.relationshipCount).toBe(1);
  });

  // ─── addRelationship ───────────────────────────────────────────────

  it('duplicate addRelationship is a no-op', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    g.addRelationship(makeRel('fn:a', 'fn:b')); // same ID
    expect(g.relationshipCount).toBe(1);
  });

  // ─── nodes / relationships arrays ──────────────────────────────────

  it('.nodes returns an array copy', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    const arr1 = g.nodes;
    const arr2 = g.nodes;
    expect(arr1).not.toBe(arr2); // different array instances
    expect(arr1).toHaveLength(1);
  });

  it('.relationships returns an array copy', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    const arr1 = g.relationships;
    const arr2 = g.relationships;
    expect(arr1).not.toBe(arr2);
    expect(arr1).toHaveLength(1);
  });

  // ─── forEachNode / forEachRelationship ──────────────────────────────

  it('forEachNode calls fn for every node', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));

    const ids: string[] = [];
    g.forEachNode((n) => ids.push(n.id));
    expect(ids).toHaveLength(2);
  });

  it('forEachRelationship calls fn for every relationship', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));

    const types: string[] = [];
    g.forEachRelationship((r) => types.push(r.type));
    expect(types).toEqual(['CALLS']);
  });

  // ─── removeRelationship ─────────────────────────────────────────────

  it('removes a relationship by id', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    expect(g.relationshipCount).toBe(1);

    const removed = g.removeRelationship('fn:a-CALLS-fn:b');
    expect(removed).toBe(true);
    expect(g.relationshipCount).toBe(0);
  });

  it('removeRelationship returns false for unknown id', () => {
    const g = createKnowledgeGraph();
    expect(g.removeRelationship('nonexistent')).toBe(false);
  });

  it('removeRelationship returns false on second call with same id', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));

    expect(g.removeRelationship('fn:a-CALLS-fn:b')).toBe(true);
    expect(g.removeRelationship('fn:a-CALLS-fn:b')).toBe(false);
  });

  it('removeRelationship does not affect nodes', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));

    g.removeRelationship('fn:a-CALLS-fn:b');
    expect(g.nodeCount).toBe(2);
    expect(g.getNode('fn:a')).toBeDefined();
    expect(g.getNode('fn:b')).toBeDefined();
  });

  it('removeRelationship leaves other relationships intact', () => {
    const g = createKnowledgeGraph();
    g.addNode(makeNode('fn:a', 'a'));
    g.addNode(makeNode('fn:b', 'b'));
    g.addNode(makeNode('fn:c', 'c'));
    g.addRelationship(makeRel('fn:a', 'fn:b'));
    g.addRelationship(makeRel('fn:b', 'fn:c'));
    expect(g.relationshipCount).toBe(2);

    g.removeRelationship('fn:a-CALLS-fn:b');
    expect(g.relationshipCount).toBe(1);
    const remaining = [...g.iterRelationships()];
    expect(remaining[0].sourceId).toBe('fn:b');
    expect(remaining[0].targetId).toBe('fn:c');
  });
});
