import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { matchGlob, parseFilters, evaluateFilter, runValidate, getChangedFiles, getMergeBase } from '../ci-filter.mjs';

// --- matchGlob ---

describe('matchGlob', () => {
	it('** matches dotfiles', () => {
		assert.ok(matchGlob('.github/workflows/ci.yml', '**'));
	});

	it('** matches deeply nested paths', () => {
		assert.ok(matchGlob('packages/cli/src/controllers/auth.ts', '**'));
	});

	it('** matches root-level files', () => {
		assert.ok(matchGlob('README.md', '**'));
	});

	it('.github/** matches workflow files', () => {
		assert.ok(matchGlob('.github/workflows/ci.yml', '.github/**'));
	});

	it('.github/** matches action files', () => {
		assert.ok(matchGlob('.github/actions/ci-filter/action.yml', '.github/**'));
	});

	it('.github/** does not match non-.github paths', () => {
		assert.ok(!matchGlob('packages/cli/src/index.ts', '.github/**'));
	});

	it('scoped package pattern matches files in that package', () => {
		assert.ok(
			matchGlob(
				'packages/@n8n/task-runner-python/src/main.py',
				'packages/@n8n/task-runner-python/**',
			),
		);
	});

	it('scoped package pattern does not match other packages', () => {
		assert.ok(!matchGlob('packages/@n8n/config/src/index.ts', 'packages/@n8n/task-runner-python/**'));
	});

	it('* matches single-level only', () => {
		assert.ok(matchGlob('README.md', '*.md'));
		assert.ok(!matchGlob('docs/README.md', '*.md'));
	});

	it('exact path match', () => {
		assert.ok(matchGlob('package.json', 'package.json'));
		assert.ok(!matchGlob('packages/cli/package.json', 'package.json'));
	});

	it('? matches single character', () => {
		assert.ok(matchGlob('file1.txt', 'file?.txt'));
		assert.ok(!matchGlob('file12.txt', 'file?.txt'));
	});

	it('**/ at start matches zero or more path segments', () => {
		assert.ok(matchGlob('src/index.ts', '**/index.ts'));
		assert.ok(matchGlob('packages/cli/src/index.ts', '**/index.ts'));
		assert.ok(matchGlob('index.ts', '**/index.ts'));
	});

	it('**/ in middle matches nested paths', () => {
		assert.ok(matchGlob('packages/@n8n/db/src/deep/file.ts', 'packages/@n8n/db/**'));
	});
});

// --- parseFilters ---

describe('parseFilters', () => {
	it('parses single-line filter', () => {
		const filters = parseFilters('workflows: .github/**');
		assert.deepEqual(filters.get('workflows'), ['.github/**']);
	});

	it('parses single-line with multiple patterns', () => {
		const filters = parseFilters('db: packages/@n8n/db/** packages/cli/**');
		assert.deepEqual(filters.get('db'), ['packages/@n8n/db/**', 'packages/cli/**']);
	});

	it('parses multi-line filter', () => {
		const input = `non-python:
  **
  !packages/@n8n/task-runner-python/**`;
		const filters = parseFilters(input);
		assert.deepEqual(filters.get('non-python'), ['**', '!packages/@n8n/task-runner-python/**']);
	});

	it('parses YAML-list-style multi-line filter', () => {
		const input = `db:
  - packages/@n8n/db/**
  - packages/cli/**`;
		const filters = parseFilters(input);
		assert.deepEqual(filters.get('db'), ['packages/@n8n/db/**', 'packages/cli/**']);
	});

	it('parses mixed single and multi-line', () => {
		const input = `non-python:
  **
  !packages/@n8n/task-runner-python/**
workflows: .github/**`;
		const filters = parseFilters(input);
		assert.equal(filters.size, 2);
		assert.deepEqual(filters.get('non-python'), ['**', '!packages/@n8n/task-runner-python/**']);
		assert.deepEqual(filters.get('workflows'), ['.github/**']);
	});

	it('ignores comments and blank lines', () => {
		const input = `# This is a comment

workflows: .github/**

# Another comment
db: packages/@n8n/db/**`;
		const filters = parseFilters(input);
		assert.equal(filters.size, 2);
	});

	it('throws on malformed input', () => {
		assert.throws(() => parseFilters('not a valid filter line'), /Malformed/);
	});

	it('throws on filter with no patterns', () => {
		const input = `empty:
other: .github/**`;
		assert.throws(() => parseFilters(input), /no patterns/);
	});
});

// --- evaluateFilter ---

describe('evaluateFilter', () => {
	it('python-only files with non-python filter returns false', () => {
		const files = [
			'packages/@n8n/task-runner-python/src/main.py',
			'packages/@n8n/task-runner-python/pyproject.toml',
		];
		const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
		assert.equal(evaluateFilter(files, patterns), false);
	});

	it('mixed python and non-python returns true', () => {
		const files = [
			'packages/@n8n/task-runner-python/src/main.py',
			'packages/cli/src/index.ts',
		];
		const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
		assert.equal(evaluateFilter(files, patterns), true);
	});

	it('non-python files with non-python filter returns true', () => {
		const files = ['packages/cli/src/index.ts', 'packages/core/src/utils.ts'];
		const patterns = ['**', '!packages/@n8n/task-runner-python/**'];
		assert.equal(evaluateFilter(files, patterns), true);
	});

	it('.github files with workflows filter returns true', () => {
		const files = ['.github/workflows/ci.yml', '.github/actions/setup/action.yml'];
		const patterns = ['.github/**'];
		assert.equal(evaluateFilter(files, patterns), true);
	});

	it('list-style parsed db filter matches db package changes', () => {
		const filters = parseFilters(`db:
  - packages/@n8n/db/**
  - packages/cli/**`);
		assert.equal(evaluateFilter(['packages/@n8n/db/src/index.ts'], filters.get('db') ?? []), true);
	});

	it('non-.github files with workflows filter returns false', () => {
		const files = ['packages/cli/src/index.ts'];
		const patterns = ['.github/**'];
		assert.equal(evaluateFilter(files, patterns), false);
	});

	it('empty changed files returns false', () => {
		assert.equal(evaluateFilter([], ['**']), false);
	});

	it('last matching pattern wins (gitignore semantics)', () => {
		const files = ['packages/@n8n/task-runner-python/src/main.py'];
		const patterns = ['**', '!packages/@n8n/task-runner-python/**', 'packages/@n8n/task-runner-python/**'];
		assert.equal(evaluateFilter(files, patterns), true);
	});
});

// --- runtime filter (E2E-chain scoping) ---

describe('runtime filter', () => {
	const runtimePatterns = [
		'**',
		'!packages/@n8n/task-runner-python/**',
		'!.github/**',
		'!**/*.md',
		'!**/LICENSE',
		'!**/CHANGELOG.md',
		'!**/*.test.ts',
		'!**/*.spec.ts',
		'!packages/testing/playwright/**',
		'!packages/frontend/@n8n/storybook/**',
	];

	it('triggers on a runtime source file', () => {
		assert.equal(evaluateFilter(['packages/cli/src/foo.ts'], runtimePatterns), true);
	});

	it('does not trigger on a unit test file', () => {
		assert.equal(evaluateFilter(['packages/cli/src/foo.test.ts'], runtimePatterns), false);
	});

	it('does not trigger on a spec file', () => {
		assert.equal(evaluateFilter(['packages/cli/src/foo.spec.ts'], runtimePatterns), false);
	});

	it('does not trigger on playwright tests', () => {
		assert.equal(
			evaluateFilter(['packages/testing/playwright/tests/x.spec.ts'], runtimePatterns),
			false,
		);
	});

	it('does not trigger on storybook files', () => {
		assert.equal(
			evaluateFilter(['packages/frontend/@n8n/storybook/preview.ts'], runtimePatterns),
			false,
		);
	});

	it('does not trigger on docs or LICENSE', () => {
		assert.equal(evaluateFilter(['README.md'], runtimePatterns), false);
		assert.equal(evaluateFilter(['LICENSE'], runtimePatterns), false);
		assert.equal(evaluateFilter(['packages/cli/LICENSE'], runtimePatterns), false);
		assert.equal(evaluateFilter(['docs/CHANGELOG.md'], runtimePatterns), false);
	});

	it('does not trigger on .github workflow changes', () => {
		assert.equal(evaluateFilter(['.github/workflows/x.yml'], runtimePatterns), false);
	});

	it('does not trigger on task-runner-python changes', () => {
		assert.equal(
			evaluateFilter(['packages/@n8n/task-runner-python/src/main.py'], runtimePatterns),
			false,
		);
	});

	it('mixed PR with source and test file triggers (any positive-match file wins)', () => {
		const files = ['packages/cli/src/foo.ts', 'packages/cli/src/foo.test.ts'];
		assert.equal(evaluateFilter(files, runtimePatterns), true);
	});
});

// --- getChangedFiles + getMergeBase (integration, exercises real git) ---

describe('getChangedFiles', () => {
	const repoDir = mkdtempSync(join(tmpdir(), 'ci-filter-'));
	const remoteDir = mkdtempSync(join(tmpdir(), 'ci-filter-remote-'));
	const originalCwd = process.cwd();
	const git = (args: string[], cwd: string = repoDir) =>
		execFileSync('git', args, { cwd, stdio: 'pipe' }).toString().trim();

	before(() => {
		// Bare remote so the action's `git fetch origin <ref>` works
		execFileSync('git', ['init', '--bare', '-b', 'main', remoteDir], { stdio: 'pipe' });
		git(['init', '-b', 'main'], repoDir);
		git(['config', 'user.email', 'test@test.local']);
		git(['config', 'user.name', 'test']);
		git(['remote', 'add', 'origin', remoteDir]);

		// Common ancestor commit
		writeFileSync(join(repoDir, 'shared.ts'), 'shared\n');
		git(['add', '.']);
		git(['commit', '-m', 'root']);
		git(['push', 'origin', 'main']);

		// PR branches off main, adds a file
		git(['checkout', '-b', 'pr-branch']);
		writeFileSync(join(repoDir, 'pr-only.ts'), 'pr\n');
		git(['add', '.']);
		git(['commit', '-m', 'PR change']);

		// Master drifts forward, modifying shared.ts (the pre-fix bug surface)
		git(['checkout', 'main']);
		writeFileSync(join(repoDir, 'shared.ts'), 'shared\ndrift-from-master\n');
		git(['commit', '-am', 'master moves']);
		git(['push', 'origin', 'main']);

		// Sit on the PR branch as if running CI
		git(['checkout', 'pr-branch']);
		process.chdir(repoDir);
	});

	after(() => {
		process.chdir(originalCwd);
		rmSync(repoDir, { recursive: true, force: true });
		rmSync(remoteDir, { recursive: true, force: true });
	});

	it('returns only PR-introduced files (master drift does not pollute)', () => {
		const changed = getChangedFiles('main');
		assert.deepEqual(changed, ['pr-only.ts']);
	});

	it('getMergeBase returns the common ancestor commit', () => {
		const mergeBase = getMergeBase();
		assert.match(mergeBase, /^[a-f0-9]{40}$/);
		const expected = git(['merge-base', 'FETCH_HEAD', 'HEAD']);
		assert.equal(mergeBase, expected);
	});

	it('rejects unsafe base refs', () => {
		assert.throws(() => getChangedFiles('main; rm -rf /'), /Unsafe/);
		assert.throws(() => getChangedFiles('main$evil'), /Unsafe/);
	});
});

// --- runValidate ---

describe('runValidate', () => {
	function runWithResults(jobResults: Record<string, { result: string }>): number | null {
		const originalEnv = process.env.INPUT_JOB_RESULTS;
		const originalExit = process.exit;
		let exitCode: number | null = null;

		process.env.INPUT_JOB_RESULTS = JSON.stringify(jobResults);
		process.exit = ((code: number) => { exitCode = code; }) as never;

		try {
			runValidate();
		} finally {
			process.env.INPUT_JOB_RESULTS = originalEnv;
			process.exit = originalExit;
		}

		return exitCode;
	}

	it('passes when all jobs succeed', () => {
		assert.equal(runWithResults({
			'install-and-build': { result: 'success' },
			'unit-test': { result: 'success' },
			typecheck: { result: 'success' },
			lint: { result: 'success' },
		}), null);
	});

	it('passes when some jobs are skipped (filtered out)', () => {
		assert.equal(runWithResults({
			'install-and-build': { result: 'success' },
			'unit-test': { result: 'success' },
			'security-checks': { result: 'skipped' },
		}), null);
	});

	it('fails when a job fails', () => {
		assert.equal(runWithResults({
			'install-and-build': { result: 'success' },
			'unit-test': { result: 'failure' },
			typecheck: { result: 'success' },
		}), 1);
	});

	it('fails when a job is cancelled', () => {
		assert.equal(runWithResults({
			'install-and-build': { result: 'success' },
			'unit-test': { result: 'cancelled' },
		}), 1);
	});

	it('fails when multiple jobs have problems', () => {
		assert.equal(runWithResults({
			'unit-test': { result: 'failure' },
			typecheck: { result: 'cancelled' },
			lint: { result: 'success' },
		}), 1);
	});
});
