import { vi } from 'vitest';

import type { N8nClient } from '../clients/n8n-client';
import { buildWorkflow } from '../harness/build-workflow';
import type { EvalLogger } from '../harness/logger';
import { buildAgentOutcome } from '../outcome/workflow-discovery';
import type { ExecutionScenario } from '../types';

// The chat loop is network/SSE machinery irrelevant to this test: stub it so a
// single-turn build reaches the pre-build seed + outcome steps without real I/O.
// (vi.mock is hoisted above the imports, so the runner picks up these stubs.)
vi.mock('../harness/chat-loop', () => ({
	SSE_SETTLE_DELAY_MS: 0,
	startSseConnection: vi.fn().mockResolvedValue(undefined),
	waitForAllActivity: vi.fn().mockResolvedValue(undefined),
	runMultiTurnConversation: vi.fn().mockResolvedValue(undefined),
	recordUserTurn: vi.fn(),
}));

// Force a "workflow built" outcome by default so the build succeeds; individual
// tests override it to simulate a build-step failure after pre-seeding.
vi.mock('../outcome/workflow-discovery', () => ({
	buildAgentOutcome: vi.fn().mockResolvedValue({
		workflowsCreated: [{ id: 'built-wf-1', name: 'Built', nodeCount: 3, active: false }],
		executionsRun: [],
		dataTablesCreated: ['built-dt-1'],
		finalText: 'done',
		workflowJsons: [{ id: 'built-wf-1', name: 'Built', nodes: [], connections: {} }],
	}),
	extractWorkflowIdsFromMessages: vi.fn().mockReturnValue([]),
}));

const silentLogger: EvalLogger = {
	info: () => {},
	verbose: () => {},
	success: () => {},
	warn: () => {},
	error: () => {},
	isVerbose: false,
};

function scenarioWithSeedTable(): ExecutionScenario {
	return {
		name: 'scenario',
		description: 'd',
		dataSetup: 'setup',
		successCriteria: 'ok',
		seedDataTables: [
			{
				id: 'job-applications-1234',
				name: 'Job Applications',
				columns: [{ name: 'id', type: 'string' as const }],
				rows: [{ id: 'row_001' }],
			},
		],
	};
}

function makeClient(overrides: Partial<Record<keyof N8nClient, unknown>> = {}): N8nClient {
	return {
		getPersonalProjectId: vi.fn().mockResolvedValue('project-1'),
		ensureThread: vi.fn().mockResolvedValue(undefined),
		setThreadCredentialAllowlist: vi.fn().mockResolvedValue(undefined),
		sendMessage: vi.fn().mockResolvedValue(undefined),
		getThreadMessages: vi.fn().mockResolvedValue({ messages: [] }),
		// Pre-build scenario-table creation returns the real id under the name.
		restoreThread: vi
			.fn()
			.mockResolvedValue({ restored: 0, workflowIds: [], dataTableIds: ['scenario-dt-1'] }),
		...overrides,
	} as unknown as N8nClient;
}

const baseConfig = {
	conversation: [{ role: 'user' as const, text: 'build a workflow' }],
	executionScenarios: [scenarioWithSeedTable()],
	skipWorkflowChecks: true,
	preRunWorkflowIds: new Set<string>(),
	claimedWorkflowIds: new Set<string>(),
	logger: silentLogger,
};

// TRUST-311 follow-up: scenario data tables are created (empty) BEFORE the build
// turn. These pin the failure/cleanup contract of that pre-build seeding:
// - a create failure fails the build as a harness problem (framework_issue),
//   and there is nothing built to leak;
// - if a LATER build step fails, the already-created tables are still handed to
//   cleanup (folded into restoredDataTableIds) rather than leaking.
describe('buildWorkflow scenario-seed data table lifecycle', () => {
	it('fails the build and flags seedingFailed when pre-build table creation fails', async () => {
		const client = makeClient({
			restoreThread: vi.fn().mockRejectedValue(new Error('seed insert failed')),
		});

		const build = await buildWorkflow({ client, ...baseConfig });

		expect(build.success).toBe(false);
		// A pre-seed failure is a harness problem, not an agent build failure — flag
		// it so the CLI attributes framework_issue, not build_failure.
		expect(build.seedingFailed).toBe(true);
		// The build never ran, so there is nothing built to leak.
		expect(build.createdWorkflowIds).toEqual([]);
		expect(build.createdDataTableIds).toEqual([]);
	});

	it('hands the pre-created scenario tables to cleanup when a later build step fails', async () => {
		vi.mocked(buildAgentOutcome).mockRejectedValueOnce(new Error('workflow discovery failed'));
		const client = makeClient(); // pre-seed succeeds → scenario-dt-1 created

		const build = await buildWorkflow({ client, ...baseConfig });

		expect(build.success).toBe(false);
		// The pre-created table must still be returned so the caller's cleanup
		// deletes it instead of leaking it into the shared project.
		expect(build.createdDataTableIds).toContain('scenario-dt-1');
	});

	// A staged prior run that produced no execution record must reach the caller even
	// when the BUILD SUCCEEDED — that is the only path where the case still gets graded,
	// and `buildFailedOnInfra` cannot catch it because it returns false for a success.
	it('carries priorRunFailed on a SUCCESSFUL build when a prior run never ran', async () => {
		const client = makeClient({
			restoreThread: vi.fn().mockResolvedValue({
				restored: 0,
				workflowIds: ['seeded-wf-1'],
				dataTableIds: ['scenario-dt-1'],
				agentIds: [],
			}),
			// Rejected before the runner: an error result with an id no execution exists under.
			executeWithLlmMock: vi.fn().mockResolvedValue({
				executionId: 'fabricated-uuid',
				success: false,
				nodeResults: {},
				errors: ['No trigger or start node found in the workflow'],
				hints: {},
				mockedCredentials: [],
			}),
			getExecution: vi.fn().mockRejectedValue(new Error('404 not found')),
			// Seeding evicts same-named leftovers before restoring.
			listWorkflows: vi.fn().mockResolvedValue([]),
		});

		const build = await buildWorkflow({
			client,
			...baseConfig,
			seed: {
				mode: 'inline' as const,
				messages: [],
				workflows: [{ id: 'sEeDeDwF1234567a', name: 'Daily Sync', nodes: [], connections: {} }],
				dataTables: [],
				agents: [],
				projects: [],
				priorRuns: [{ workflow: 'sEeDeDwF1234567a' }],
			},
		});

		expect(build.success).toBe(true);
		expect(build.priorRunFailed).toContain('sEeDeDwF1234567a');
	});

	// A throw from staging is an authoring/harness fault. Without `seedingFailed` the
	// outer catch returns a plain failed build and the case is scored `build_failure` /
	// `builder_issue` — a builder red for something the builder had no part in.
	it('flags seedingFailed when a prior run names an id the seed never created', async () => {
		const client = makeClient({
			restoreThread: vi.fn().mockResolvedValue({
				restored: 0,
				workflowIds: [],
				dataTableIds: ['scenario-dt-1'],
				agentIds: [],
			}),
			listWorkflows: vi.fn().mockResolvedValue([]),
		});

		const build = await buildWorkflow({
			client,
			...baseConfig,
			seed: {
				mode: 'inline' as const,
				messages: [],
				workflows: [{ id: 'sEeDeDwF1234567a', name: 'Daily Sync', nodes: [], connections: {} }],
				dataTables: [],
				agents: [],
				projects: [],
				// Not the declared id — `executePriorRuns` throws.
				priorRuns: [{ workflow: 'nOtDeClArEd1234a' }],
			},
		});

		expect(build.success).toBe(false);
		expect(build.seedingFailed).toBe(true);
	});

	it('returns the built workflow, both tables, and the name→id map on success', async () => {
		const client = makeClient();

		const build = await buildWorkflow({ client, ...baseConfig });

		expect(build.success).toBe(true);
		expect(build.createdWorkflowIds).toContain('built-wf-1');
		expect(build.createdDataTableIds).toEqual(
			expect.arrayContaining(['built-dt-1', 'scenario-dt-1']),
		);
		// The name→real-id map lets each scenario reseed rows into the bound table.
		expect(build.seededScenarioTableIdsByName).toEqual({ 'Job Applications': 'scenario-dt-1' });
	});
});

describe('buildWorkflow declared credentials', () => {
	it('registers the seeded credentials as passing their connection test', async () => {
		const setThreadCredentialAllowlist = vi.fn().mockResolvedValue(undefined);
		const client = makeClient({
			setThreadCredentialAllowlist,
			createCredential: vi.fn().mockResolvedValue({ id: 'cred-seeded' }),
		});

		const build = await buildWorkflow({
			client,
			...baseConfig,
			credentials: [{ type: 'slackApi' }],
		});

		expect(build.success).toBe(true);
		// A declared credential stands for one the user already connected, so its
		// placeholder token must not make the build see a failing connection test.
		expect(setThreadCredentialAllowlist).toHaveBeenCalledWith(
			expect.any(String),
			['cred-seeded'],
			['cred-seeded'],
		);
	});

	it('creates a blank credential with no field values and keeps it off the bypass list', async () => {
		const setThreadCredentialAllowlist = vi.fn().mockResolvedValue(undefined);
		const createCredential = vi.fn().mockResolvedValue({ id: 'cred-blank' });
		const client = makeClient({ setThreadCredentialAllowlist, createCredential });

		const build = await buildWorkflow({
			client,
			...baseConfig,
			credentials: [{ type: 'httpHeaderAuth', blank: true }],
		});

		expect(build.success).toBe(true);
		// A blank credential models one the user saved without filling anything in,
		// so it is seeded with no data and must never resolve a test as passing.
		expect(createCredential).toHaveBeenCalledWith(expect.any(String), 'httpHeaderAuth', {});
		expect(setThreadCredentialAllowlist).toHaveBeenCalledWith(
			expect.any(String),
			['cred-blank'],
			[],
		);
	});

	it('filters an already-broken credential out of the connection-test bypass list', async () => {
		const setThreadCredentialAllowlist = vi.fn().mockResolvedValue(undefined);
		const createCredential = vi
			.fn()
			.mockResolvedValueOnce({ id: 'cred-working' })
			.mockResolvedValueOnce({ id: 'cred-broken' });
		const client = makeClient({ setThreadCredentialAllowlist, createCredential });

		const build = await buildWorkflow({
			client,
			...baseConfig,
			credentials: [{ type: 'slackApi' }, { type: 'notionApi', valid: false }],
		});

		expect(build.success).toBe(true);
		// Both credentials are created for real and visible to the build (2nd arg) —
		// only the one NOT marked already-broken bypasses its connection test (3rd arg).
		expect(setThreadCredentialAllowlist).toHaveBeenCalledWith(
			expect.any(String),
			['cred-working', 'cred-broken'],
			['cred-working'],
		);
	});
});
