import { describe, it, expect } from 'vitest';
import {
	emptyImageState,
	emptyComposeState,
	emptyStaticState,
	emptyDockerfileState,
	seedImageState,
	seedComposeState,
	seedStaticState,
	seedDockerfileState,
	imageToConfig,
	composeToConfig,
	staticToConfig,
	dockerfileToConfig,
	stringifyConfig,
	isImageValid,
	isComposeValid,
	isStaticValid,
	isDockerfileValid
} from './sourceForms';

describe('image source', () => {
	it('seeds defaults from empty/malformed JSON', () => {
		expect(seedImageState('{}')).toEqual(emptyImageState());
		expect(seedImageState('not json')).toEqual(emptyImageState());
		expect(seedImageState('[]')).toEqual(emptyImageState());
		expect(seedImageState('42')).toEqual(emptyImageState());
	});

	it('seeds populated fields', () => {
		const json = JSON.stringify({
			image: 'nginx',
			port: 8080,
			healthcheck: '/healthz',
			default_tag: 'stable',
			registry_name: 'docker.io',
			cpu_limit: 2,
			memory_limit: 512,
			max_instances: 3
		});
		expect(seedImageState(json)).toEqual({
			ref: 'nginx',
			port: 8080,
			healthcheck: '/healthz',
			defaultTag: 'stable',
			registryName: 'docker.io',
			cpuLimit: 2,
			memoryLimit: 512,
			maxInstances: 3,
			deployStrategy: ''
		});
	});

	it('serializes to the exact source_config shape and key order', () => {
		const config = imageToConfig(emptyImageState(), '{}');
		expect(Object.keys(config)).toEqual([
			'image',
			'registry_name',
			'port',
			'healthcheck',
			'env',
			'volumes',
			'cpu_limit',
			'memory_limit',
			'default_tag',
			'max_instances'
		]);
		expect(config).toEqual({
			image: '',
			registry_name: '',
			port: 0,
			healthcheck: '',
			env: {},
			volumes: [],
			cpu_limit: 0,
			memory_limit: 0,
			default_tag: 'latest',
			max_instances: 1
		});
	});

	it('preserves env and volumes from the existing config', () => {
		const existing = JSON.stringify({
			image: 'old',
			env: { FOO: 'bar' },
			volumes: [{ source: 'data', scope: 'named' }]
		});
		const config = imageToConfig({ ...emptyImageState(), ref: 'new' }, existing);
		expect(config.env).toEqual({ FOO: 'bar' });
		expect(config.volumes).toEqual([{ source: 'data', scope: 'named' }]);
		expect(config.image).toBe('new');
	});

	it('round-trips state -> config -> state', () => {
		const state = seedImageState(
			JSON.stringify({ image: 'app', port: 3000, default_tag: 'v1', max_instances: 2 })
		);
		expect(seedImageState(stringifyConfig(imageToConfig(state, '{}')))).toEqual(state);
	});

	it('validity requires a non-empty image ref', () => {
		expect(isImageValid(emptyImageState())).toBe(false);
		expect(isImageValid({ ...emptyImageState(), ref: '  ' })).toBe(false);
		expect(isImageValid({ ...emptyImageState(), ref: 'nginx' })).toBe(true);
	});
});

describe('compose source', () => {
	it('seeds defaults and populated fields', () => {
		expect(seedComposeState('{}')).toEqual(emptyComposeState());
		expect(
			seedComposeState(JSON.stringify({ compose_yaml: 'services: {}', compose_project_name: 'app' }))
		).toEqual({ yaml: 'services: {}', projectName: 'app' });
	});

	it('serializes to the exact shape', () => {
		const config = composeToConfig({ yaml: 'x', projectName: 'p' });
		expect(Object.keys(config)).toEqual(['compose_yaml', 'compose_project_name']);
		expect(config).toEqual({ compose_yaml: 'x', compose_project_name: 'p' });
	});

	it('validity requires non-empty yaml', () => {
		expect(isComposeValid(emptyComposeState())).toBe(false);
		expect(isComposeValid({ yaml: 'services: {}', projectName: '' })).toBe(true);
	});
});

describe('static source', () => {
	it('seeds defaults, normalizing provider and branch', () => {
		expect(seedStaticState('{}')).toEqual(emptyStaticState());
		// unknown provider -> gitea; empty branch -> main
		expect(seedStaticState(JSON.stringify({ provider: 'bogus', branch: '' }))).toEqual(
			emptyStaticState()
		);
		expect(seedStaticState(JSON.stringify({ provider: 'github' })).provider).toBe('github');
		expect(seedStaticState(JSON.stringify({ mode: 'deno' })).mode).toBe('deno');
	});

	it('serializes to the exact shape and key order', () => {
		const config = staticToConfig(emptyStaticState(), '{}');
		expect(Object.keys(config)).toEqual([
			'provider',
			'base_url',
			'repo_owner',
			'repo_name',
			'branch',
			'folder_path',
			'access_token',
			'mode',
			'render_markdown',
			'report_commit_status'
		]);
		expect(config.branch).toBe('main');
		expect(config.report_commit_status).toBe(false);
	});

	it('seeds and serializes report_commit_status', () => {
		expect(seedStaticState(JSON.stringify({ report_commit_status: true })).reportCommitStatus).toBe(
			true
		);
		const cfg = staticToConfig({ ...emptyStaticState(), reportCommitStatus: true }, '{}');
		expect(cfg.report_commit_status).toBe(true);
	});

	it('preserves storage_* keys only when present', () => {
		const withStorage = staticToConfig(
			emptyStaticState(),
			JSON.stringify({ storage_enabled: true, storage_limit_mb: 100 })
		);
		expect(withStorage.storage_enabled).toBe(true);
		expect(withStorage.storage_limit_mb).toBe(100);

		const without = staticToConfig(emptyStaticState(), '{}');
		expect('storage_enabled' in without).toBe(false);
		expect('storage_limit_mb' in without).toBe(false);
	});

	it('round-trips a populated state', () => {
		const state = seedStaticState(
			JSON.stringify({
				provider: 'gitlab',
				base_url: 'https://gl.example',
				repo_owner: 'me',
				repo_name: 'site',
				branch: 'dev',
				folder_path: 'public',
				access_token: 'secret',
				mode: 'deno',
				render_markdown: true
			})
		);
		expect(seedStaticState(stringifyConfig(staticToConfig(state, '{}')))).toEqual(state);
	});

	it('validity requires base_url + owner + repo', () => {
		expect(isStaticValid(emptyStaticState())).toBe(false);
		expect(
			isStaticValid({
				...emptyStaticState(),
				baseURL: 'https://x',
				repoOwner: 'o',
				repoName: 'r'
			})
		).toBe(true);
	});
});

describe('dockerfile source', () => {
	it('seeds defaults, defaulting dockerfile_path to Dockerfile', () => {
		expect(seedDockerfileState('{}')).toEqual(emptyDockerfileState());
		expect(seedDockerfileState(JSON.stringify({ dockerfile_path: '' })).dockerfilePath).toBe(
			'Dockerfile'
		);
		expect(seedDockerfileState(JSON.stringify({ dockerfile_path: 'docker/Dockerfile' })).dockerfilePath).toBe(
			'docker/Dockerfile'
		);
	});

	it('serializes to the exact shape and key order', () => {
		const config = dockerfileToConfig(emptyDockerfileState(), '{}');
		expect(Object.keys(config)).toEqual([
			'provider',
			'base_url',
			'repo_owner',
			'repo_name',
			'branch',
			'access_token',
			'context_path',
			'dockerfile_path',
			'port',
			'report_commit_status'
		]);
		expect(config.dockerfile_path).toBe('Dockerfile');
		expect(config.branch).toBe('main');
		expect(config.port).toBe(0);
		expect(config.report_commit_status).toBe(false);
	});

	it('seeds and serializes report_commit_status', () => {
		expect(
			seedDockerfileState(JSON.stringify({ report_commit_status: true })).reportCommitStatus
		).toBe(true);
		const cfg = dockerfileToConfig({ ...emptyDockerfileState(), reportCommitStatus: true }, '{}');
		expect(cfg.report_commit_status).toBe(true);
	});

	it('preserves unknown keys but scrubs static-only keys', () => {
		const existing = JSON.stringify({
			// unknown key the operator added via raw JSON -> preserved
			healthcheck: '/up',
			cpu_limit: 1,
			// static-only leftovers from a static->dockerfile switch -> scrubbed
			folder_path: 'public',
			mode: 'deno',
			render_markdown: true,
			storage_enabled: true,
			storage_limit_mb: 50
		});
		const config = dockerfileToConfig(emptyDockerfileState(), existing);
		expect(config.healthcheck).toBe('/up');
		expect(config.cpu_limit).toBe(1);
		expect('folder_path' in config).toBe(false);
		expect('mode' in config).toBe(false);
		expect('render_markdown' in config).toBe(false);
		expect('storage_enabled' in config).toBe(false);
		expect('storage_limit_mb' in config).toBe(false);
	});

	it('round-trips a populated state', () => {
		const state = seedDockerfileState(
			JSON.stringify({
				provider: 'github',
				base_url: 'https://gh.example',
				repo_owner: 'me',
				repo_name: 'svc',
				branch: 'main',
				access_token: 't',
				context_path: 'backend',
				dockerfile_path: 'backend/Dockerfile',
				port: 8000
			})
		);
		expect(seedDockerfileState(stringifyConfig(dockerfileToConfig(state, '{}')))).toEqual(state);
	});

	it('validity requires git fields + a positive port', () => {
		const base = {
			...emptyDockerfileState(),
			baseURL: 'https://x',
			repoOwner: 'o',
			repoName: 'r'
		};
		expect(isDockerfileValid(base)).toBe(false); // port 0
		expect(isDockerfileValid({ ...base, port: 8080 })).toBe(true);
		expect(isDockerfileValid({ ...base, port: -1 })).toBe(false);
	});
});

describe('deploy_strategy (cross-source)', () => {
	it('seeds recognized strategies and drops junk to ""', () => {
		expect(seedImageState(JSON.stringify({ image: 'x', deploy_strategy: 'recreate' })).deployStrategy).toBe('recreate');
		expect(seedImageState(JSON.stringify({ image: 'x', deploy_strategy: 'blue-green' })).deployStrategy).toBe('blue-green');
		expect(seedImageState(JSON.stringify({ image: 'x', deploy_strategy: 'rolling' })).deployStrategy).toBe('');
		expect(seedDockerfileState(JSON.stringify({ deploy_strategy: 'blue-green' })).deployStrategy).toBe('blue-green');
		expect(seedStaticState(JSON.stringify({ deploy_strategy: 'recreate' })).deployStrategy).toBe('recreate');
		expect(seedStaticState(JSON.stringify({ deploy_strategy: 'nope' })).deployStrategy).toBe('');
	});

	it('omits deploy_strategy when empty so existing configs stay byte-identical', () => {
		expect('deploy_strategy' in imageToConfig(emptyImageState(), '{}')).toBe(false);
		expect('deploy_strategy' in dockerfileToConfig(emptyDockerfileState(), '{}')).toBe(false);
		expect('deploy_strategy' in staticToConfig(emptyStaticState(), '{}')).toBe(false);
	});

	it('emits deploy_strategy at the end of the owned block when set', () => {
		const img = imageToConfig({ ...emptyImageState(), deployStrategy: 'recreate' }, '{}');
		expect(img.deploy_strategy).toBe('recreate');
		expect(Object.keys(img).at(-1)).toBe('deploy_strategy');

		const df = dockerfileToConfig({ ...emptyDockerfileState(), deployStrategy: 'blue-green' }, '{}');
		expect(df.deploy_strategy).toBe('blue-green');
		expect(Object.keys(df).at(-1)).toBe('deploy_strategy');

		const st = staticToConfig({ ...emptyStaticState(), deployStrategy: 'blue-green' }, '{}');
		expect(st.deploy_strategy).toBe('blue-green');
		expect(Object.keys(st).at(-1)).toBe('deploy_strategy');
	});

	it('dockerfile owns deploy_strategy: form value wins, stale value scrubbed', () => {
		// Form value overrides an existing stored strategy (owned key).
		const overridden = dockerfileToConfig(
			{ ...emptyDockerfileState(), deployStrategy: 'blue-green' },
			JSON.stringify({ deploy_strategy: 'recreate', healthcheck: '/up' })
		);
		expect(overridden.deploy_strategy).toBe('blue-green');
		expect(overridden.healthcheck).toBe('/up'); // unknown key still preserved

		// Clearing back to default scrubs the stored key (no orphan recreate).
		const cleared = dockerfileToConfig(
			emptyDockerfileState(),
			JSON.stringify({ deploy_strategy: 'blue-green' })
		);
		expect('deploy_strategy' in cleared).toBe(false);
	});

	it('round-trips an explicit strategy through serialize -> seed', () => {
		const s = seedImageState(JSON.stringify({ image: 'app', deploy_strategy: 'recreate' }));
		expect(seedImageState(stringifyConfig(imageToConfig(s, '{}'))).deployStrategy).toBe('recreate');
	});
});
