import stream from 'node:stream';
import http from 'node:http';

import AbortController from 'abort-controller';
import chai from 'chai';
import FormData from 'form-data';
import Blob from 'fetch-blob';

import {Request} from '../src/index.js';
import TestServer from './utils/server.js';

const {expect} = chai;

describe('Request', () => {
	const local = new TestServer();
	let base;

	before(async () => {
		await local.start();
		base = `http://${local.hostname}:${local.port}/`;
	});

	after(async () => {
		return local.stop();
	});

	it('should have attributes conforming to Web IDL', () => {
		const request = new Request('https://github.com/');
		const enumerableProperties = [];
		for (const property in request) {
			enumerableProperties.push(property);
		}

		for (const toCheck of [
			'body',
			'bodyUsed',
			'arrayBuffer',
			'blob',
			'json',
			'text',
			'method',
			'url',
			'headers',
			'redirect',
			'clone',
			'signal'
		]) {
			expect(enumerableProperties).to.contain(toCheck);
		}

		for (const toCheck of [
			'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal'
		]) {
			expect(() => {
				request[toCheck] = 'abc';
			}).to.throw();
		}
	});

	it('should support wrapping Request instance', () => {
		const url = `${base}hello`;

		const form = new FormData();
		form.append('a', '1');
		const {signal} = new AbortController();

		const r1 = new Request(url, {
			method: 'POST',
			follow: 1,
			body: form,
			signal
		});
		const r2 = new Request(r1, {
			follow: 2
		});

		expect(r2.url).to.equal(url);
		expect(r2.method).to.equal('POST');
		expect(r2.signal).to.equal(signal);
		// Note that we didn't clone the body
		expect(r2.body).to.equal(form);
		expect(r1.follow).to.equal(1);
		expect(r2.follow).to.equal(2);
		expect(r1.counter).to.equal(0);
		expect(r2.counter).to.equal(0);
	});

	it('should override signal on derived Request instances', () => {
		const parentAbortController = new AbortController();
		const derivedAbortController = new AbortController();
		const parentRequest = new Request(`${base}hello`, {
			signal: parentAbortController.signal
		});
		const derivedRequest = new Request(parentRequest, {
			signal: derivedAbortController.signal
		});
		expect(parentRequest.signal).to.equal(parentAbortController.signal);
		expect(derivedRequest.signal).to.equal(derivedAbortController.signal);
	});

	it('should allow removing signal on derived Request instances', () => {
		const parentAbortController = new AbortController();
		const parentRequest = new Request(`${base}hello`, {
			signal: parentAbortController.signal
		});
		const derivedRequest = new Request(parentRequest, {
			signal: null
		});
		expect(parentRequest.signal).to.equal(parentAbortController.signal);
		expect(derivedRequest.signal).to.equal(null);
	});

	it('should throw error with GET/HEAD requests with body', () => {
		expect(() => new Request(base, {body: ''}))
			.to.throw(TypeError);
		expect(() => new Request(base, {body: 'a'}))
			.to.throw(TypeError);
		expect(() => new Request(base, {body: '', method: 'HEAD'}))
			.to.throw(TypeError);
		expect(() => new Request(base, {body: 'a', method: 'HEAD'}))
			.to.throw(TypeError);
		expect(() => new Request(base, {body: 'a', method: 'get'}))
			.to.throw(TypeError);
		expect(() => new Request(base, {body: 'a', method: 'head'}))
			.to.throw(TypeError);
		expect(() => new Request(new Request(base), {body: 'a'}))
			.to.throw(TypeError);
	});

	it('should throw error when including credentials', () => {
		expect(() => new Request('https://john:pass@github.com/'))
			.to.throw(TypeError);
		expect(() => new Request(new URL('https://john:pass@github.com/')))
			.to.throw(TypeError);
	});

	it('should default to null as body', () => {
		const request = new Request(base);
		expect(request.body).to.equal(null);
		return request.text().then(result => expect(result).to.equal(''));
	});

	it('should support parsing headers', () => {
		const url = base;
		const request = new Request(url, {
			headers: {
				a: '1'
			}
		});
		expect(request.url).to.equal(url);
		expect(request.headers.get('a')).to.equal('1');
	});

	it('should uppercase DELETE, GET, HEAD, OPTIONS, POST and PUT methods', () => {
		const url = base;
		for (const method of ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) {
			const request = new Request(url, {
				method: method.toLowerCase()
			});
			expect(request.method).to.equal(method);
		}
	});

	it('should not uppercase unknown methods and patch', () => {
		const url = base;
		for (const method of ['patch', 'chicken']) {
			const request = new Request(url, {method});
			expect(request.method).to.equal(method);
		}
	});

	it('should support arrayBuffer() method', () => {
		const url = base;
		const request = new Request(url, {
			method: 'POST',
			body: 'a=1'
		});
		expect(request.url).to.equal(url);
		return request.arrayBuffer().then(result => {
			expect(result).to.be.an.instanceOf(ArrayBuffer);
			const string = String.fromCharCode.apply(null, new Uint8Array(result));
			expect(string).to.equal('a=1');
		});
	});

	it('should support text() method', () => {
		const url = base;
		const request = new Request(url, {
			method: 'POST',
			body: 'a=1'
		});
		expect(request.url).to.equal(url);
		return request.text().then(result => {
			expect(result).to.equal('a=1');
		});
	});

	it('should support json() method', () => {
		const url = base;
		const request = new Request(url, {
			method: 'POST',
			body: '{"a":1}'
		});
		expect(request.url).to.equal(url);
		return request.json().then(result => {
			expect(result.a).to.equal(1);
		});
	});

	it('should support buffer() method', () => {
		const url = base;
		const request = new Request(url, {
			method: 'POST',
			body: 'a=1'
		});
		expect(request.url).to.equal(url);
		return request.buffer().then(result => {
			expect(result.toString()).to.equal('a=1');
		});
	});

	it('should support blob() method', async () => {
		const url = base;
		const request = new Request(url, {
			method: 'POST',
			body: new TextEncoder().encode('a=1')
		});
		expect(request.url).to.equal(url);
		const blob = await request.blob();
		expect(blob).to.be.an.instanceOf(Blob);
		expect(blob.size).to.equal(3);
		expect(blob.type).to.equal('');
	});

	it('should support clone() method', () => {
		const url = base;
		const body = stream.Readable.from('a=1');
		const agent = new http.Agent();
		const {signal} = new AbortController();
		const request = new Request(url, {
			body,
			method: 'POST',
			redirect: 'manual',
			headers: {
				b: '2'
			},
			follow: 3,
			compress: false,
			agent,
			signal
		});
		const cl = request.clone();
		expect(cl.url).to.equal(url);
		expect(cl.method).to.equal('POST');
		expect(cl.redirect).to.equal('manual');
		expect(cl.headers.get('b')).to.equal('2');
		expect(cl.follow).to.equal(3);
		expect(cl.compress).to.equal(false);
		expect(cl.method).to.equal('POST');
		expect(cl.counter).to.equal(0);
		expect(cl.agent).to.equal(agent);
		expect(cl.signal).to.equal(signal);
		// Clone body shouldn't be the same body
		expect(cl.body).to.not.equal(body);
		return Promise.all([cl.text(), request.text()]).then(results => {
			expect(results[0]).to.equal('a=1');
			expect(results[1]).to.equal('a=1');
		});
	});

	it('should support ArrayBuffer as body', () => {
		const encoder = new TextEncoder();
		const request = new Request(base, {
			method: 'POST',
			body: encoder.encode('a=1').buffer
		});
		return request.text().then(result => {
			expect(result).to.equal('a=1');
		});
	});

	it('should support Uint8Array as body', () => {
		const encoder = new TextEncoder();
		const request = new Request(base, {
			method: 'POST',
			body: encoder.encode('a=1')
		});
		return request.text().then(result => {
			expect(result).to.equal('a=1');
		});
	});

	it('should support DataView as body', () => {
		const encoder = new TextEncoder();
		const request = new Request(base, {
			method: 'POST',
			body: new DataView(encoder.encode('a=1').buffer)
		});
		return request.text().then(result => {
			expect(result).to.equal('a=1');
		});
	});

	it('should warn once when using .data (request)', () => new Promise(resolve => {
		process.once('warning', evt => {
			expect(evt.message).to.equal('.data is not a valid RequestInit property, use .body instead');
			resolve();
		});

		// eslint-disable-next-line no-new
		new Request(base, {
			data: ''
		});
	}));
});
