import { SourceCode } from 'eslint';
import type { Node } from 'estree';
import { describe, expect, it, vi } from 'vitest';

import {
  checkClassNames,
  findAndReportMatches,
  getLocationForMatch,
} from './eslint-utils.mjs';

function createSourceCode(value = '', line = 1) {
  return {
    getText: vi.fn(() => value),
    getLocFromIndex: vi.fn((index) => ({
      line,
      column: index,
    })),
  } as unknown as SourceCode; // blunt instrument
}

describe('getLocationForMatch', () => {
  it('returns location for string literal with quotes', () => {
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
      range: [0, 10],
    } as Node;
    const sourceCode = createSourceCode('"hello"');

    const result = getLocationForMatch(node, 2, 5, sourceCode);

    expect(sourceCode.getText).toHaveBeenCalledWith(node);
    // Should account for opening quote (offset 1) + match start (2)
    expect(sourceCode.getLocFromIndex).toHaveBeenCalledWith(3); // 0 + 1 + 2
    expect(sourceCode.getLocFromIndex).toHaveBeenCalledWith(6); // 0 + 1 + 5
    expect(result).toEqual({
      start: { line: 1, column: 3 },
      end: { line: 1, column: 6 },
    });
  });

  it('returns location for template element', () => {
    const node = {
      type: 'TemplateElement',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
      range: [5, 15],
    } as Node;
    const sourceCode = createSourceCode();

    const result = getLocationForMatch(node, 2, 5, sourceCode);

    expect(sourceCode.getLocFromIndex).toHaveBeenCalledWith(7); // 5 + 2
    expect(sourceCode.getLocFromIndex).toHaveBeenCalledWith(10); // 5 + 5
    expect(result).toEqual({
      start: { line: 1, column: 7 },
      end: { line: 1, column: 10 },
    });
  });

  it('returns node loc as fallback for other node types', () => {
    const nodeLoc = {
      start: { line: 1, column: 0 },
      end: { line: 1, column: 10 },
    };
    const node = {
      type: 'Identifier',
      loc: nodeLoc,
      range: [0, 10],
    } as Node;
    const sourceCode = createSourceCode();

    const result = getLocationForMatch(node, 0, 5, sourceCode);

    expect(result).toBe(nodeLoc);
  });
});

describe('findAndReportMatches', () => {
  it('finds and reports all regex matches', () => {
    const regex = /\b\w+-\[#[0-9a-fA-F]+\]/g;
    const value = 'text-[#fff] bg-[#000]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 20 } },
      range: [0, 20],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };

    findAndReportMatches({
      regex,
      value,
      node,
      context,
      messageId: 'testMessage',
      getMessageData: (match) => ({ className: match[0] }),
      sourceCode,
    });

    expect(context.report).toHaveBeenCalledTimes(2);
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        messageId: 'testMessage',
        data: { className: 'text-[#fff]' },
      })
    );
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        messageId: 'testMessage',
        data: { className: 'bg-[#000]' },
      })
    );
  });

  it('respects shouldReport filter', () => {
    const regex = /\b\w+-\[([a-z]+)\]/gi;
    const value = 'text-[red] bg-[invalid]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 20 } },
      range: [0, 20],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const colorNames = ['red', 'blue', 'green'];

    findAndReportMatches({
      regex,
      value,
      node,
      context,
      messageId: 'testMessage',
      getMessageData: (match) => ({ className: match[0] }),
      shouldReport: (match) => colorNames.includes(match[1].toLowerCase()),
      sourceCode,
    });

    // Only 'text-[red]' should be reported, not 'bg-[invalid]'
    expect(context.report).toHaveBeenCalledTimes(1);
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { className: 'text-[red]' },
      })
    );
  });

  it('resets regex lastIndex between calls', () => {
    const regex = /test/g;
    const value = 'test test';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
      range: [0, 10],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };

    // First call
    findAndReportMatches({
      regex,
      value,
      node,
      context,
      messageId: 'testMessage',
      getMessageData: (match) => ({ text: match[0] }),
      sourceCode,
    });

    // Second call should find matches again
    context.report.mockClear();
    findAndReportMatches({
      regex,
      value,
      node,
      context,
      messageId: 'testMessage',
      getMessageData: (match) => ({ text: match[0] }),
      sourceCode,
    });

    expect(context.report).toHaveBeenCalledTimes(2);
  });
});

describe('checkClassNames', () => {
  it('splits value by whitespace and tests each classname individually', () => {
    const value = 'flex items-center text-[#fff]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 30 } },
      range: [0, 30],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    // Should only report 'text-[#fff]', not 'flex' or 'items-center'
    expect(context.report).toHaveBeenCalledTimes(1);
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        messageId: 'hexColor',
        data: { className: 'text-[#fff]' },
      })
    );
  });

  it('handles multiple spaces between classnames', () => {
    const value = 'text-[#fff]  bg-[#000]   border-[#333]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 40 } },
      range: [0, 40],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).toHaveBeenCalledTimes(3);
  });

  it('filters empty strings from split', () => {
    const value = '  text-[#fff]  ';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 15 } },
      range: [0, 15],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).toHaveBeenCalledTimes(1);
  });

  it('tests against multiple matchers', () => {
    const value = 'text-[#fff] bg-[rgb(255,0,0)]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 30 } },
      range: [0, 30],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
      {
        regex: /\b\w+-\[rgba?\([^\]]+\)\]/gi,
        messageId: 'rgbColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).toHaveBeenCalledTimes(2);
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        messageId: 'hexColor',
        data: { className: 'text-[#fff]' },
      })
    );
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        messageId: 'rgbColor',
        data: { className: 'bg-[rgb(255,0,0)]' },
      })
    );
  });

  it('respects shouldReport filter in matchers', () => {
    const value = 'text-[red] bg-[invalid] border-[blue]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 40 } },
      range: [0, 40],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const colorNames = new Set(['red', 'blue', 'green']);
    const matchers = [
      {
        regex: /\b\w+-\[([a-z]+)\]/gi,
        messageId: 'colorName',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
        shouldReport: (match: RegExpExecArray) =>
          colorNames.has(match[1]?.toLowerCase() ?? ''),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    // Should report 'text-[red]' and 'border-[blue]', but not 'bg-[invalid]'
    expect(context.report).toHaveBeenCalledTimes(2);
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { className: 'text-[red]' },
      })
    );
    expect(context.report).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { className: 'border-[blue]' },
      })
    );
  });

  it('resets regex lastIndex after each test', () => {
    const value = 'text-[#fff] bg-[#000]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 22 } },
      range: [0, 22],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const regex = /\b\w+-\[#[0-9a-fA-F]+\]/g;
    const matchers = [
      {
        regex,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    // Regex should be reset and work correctly for both classnames
    expect(context.report).toHaveBeenCalledTimes(2);
    expect(regex.lastIndex).toBe(0);
  });

  it('calculates correct match positions in original value', () => {
    const value = 'flex text-[#fff] items-center';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 30 } },
      range: [10, 40], // Node starts at position 10
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).toHaveBeenCalledTimes(1);
    // 'text-[#fff]' starts at index 5 in the value string
    // With node range starting at 10 and accounting for opening quote (+1)
    // The match should start at 10 + 1 + 5 = 16
    expect(sourceCode.getLocFromIndex).toHaveBeenCalledWith(16);
  });

  it('handles empty value', () => {
    const value = '';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
      range: [0, 0],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /test/g,
        messageId: 'test',
        getMessageData: (match: RegExpExecArray) => ({ text: match[0] }),
      },
    ];

    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).not.toHaveBeenCalled();
  });

  it('uses default sourceCode from context if not provided', () => {
    const value = 'text-[#fff]';
    const node = {
      type: 'Literal',
      loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 15 } },
      range: [0, 15],
    } as Node;
    const sourceCode = createSourceCode(`"${value}"`);
    const context = {
      report: vi.fn(),
      sourceCode,
    };
    const matchers = [
      {
        regex: /\b\w+-\[#[0-9a-fA-F]+\]/g,
        messageId: 'hexColor',
        getMessageData: (match: RegExpExecArray) => ({ className: match[0] }),
      },
    ];

    // Don't provide sourceCode parameter
    checkClassNames({
      value,
      node,
      context,
      matchers,
    });

    expect(context.report).toHaveBeenCalledTimes(1);
    expect(sourceCode.getLocFromIndex).toHaveBeenCalled();
  });
});
