/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.guice;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

import jakarta.inject.Named;

import org.apache.wicket.Session;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.mock.MockApplication;
import org.apache.wicket.mock.MockWebRequest;
import org.apache.wicket.protocol.http.WebSession;
import org.apache.wicket.protocol.http.mock.MockServletContext;
import org.apache.wicket.request.Url;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;

/**
 */
public abstract class AbstractInjectorTest
{

	protected abstract TestNoComponentInterface newTestNoComponent();

	protected abstract TestComponentInterface newTestComponent(String id);

	private final MockApplication app = new MockApplication();

	@BeforeEach
	public void before()
	{
		app.setServletContext(new MockServletContext(app, null));

		ThreadContext.setApplication(app);

		app.setName(getClass().getName());
		app.initApplication();

		Session session = new WebSession(new MockWebRequest(Url.parse("/")));
		app.getSessionStore().bind(null, session);
		ThreadContext.setSession(session);

		GuiceComponentInjector injector = new GuiceComponentInjector(app, new Module()
		{
			@Override
			public void configure(final Binder binder)
			{
				binder.bind(ITestService.class).to(TestService.class);
				binder.bind(ITestService.class).annotatedWith(Red.class).to(TestServiceRed.class);
				binder.bind(ITestService.class).annotatedWith(Blue.class).to(TestServiceBlue.class);
				binder.bind(new TypeLiteral<Map<String, String>>()
				{
				}).toProvider(new Provider<Map<String, String>>()
				{
					@Override
					public Map<String, String> get()
					{
						Map<String, String> strings = new HashMap<>();

						strings.put(ITestService.RESULT, ITestService.RESULT);

						return strings;
					}
				});

				binder.bind(String.class).annotatedWith(Names.named("named1")).toInstance(
					"NAMED_1");
				binder.bind(String.class).annotatedWith(Names.named("named2")).toInstance(
					"NAMED_2");

				binder.bind(String.class).annotatedWith(new Jsr330Named("named1")).toInstance(
					"NAMED_1");
				binder.bind(String.class).annotatedWith(new Jsr330Named("named2")).toInstance(
					"NAMED_2");
				binder.bind(EvilTestService.class).toInstance(new EvilTestService("evil123", 5));
			}

		});
		app.getComponentInstantiationListeners().add(injector);
	}

	@AfterEach
	public void after()
	{
		app.internalDestroy();
		ThreadContext.detach();
	}

	/**
	 * testInjectionAndSerialization()
	 */
	@Test
	public void testInjectionAndSerialization()
	{
		// Create a new component, which should be automatically injected,
		// and test to make sure the injection has worked.
		TestComponentInterface testComponent = newTestComponent("id");
		doChecksForComponent(testComponent);

		// Serialize and deserialize the object, and check it still works.
		TestComponentInterface clonedComponent = WicketObjects
			.cloneObject(testComponent);
		doChecksForComponent(clonedComponent);

		// Test injection of a class that does not extend Component
		TestNoComponentInterface noncomponent = newTestNoComponent();
		doChecksForNoComponent(noncomponent);
	}

	private void doChecksForNoComponent(final TestNoComponentInterface noncomponent)
	{
		assertEquals(ITestService.RESULT_RED, noncomponent.getString());
	}

	private void doChecksForComponent(final TestComponentInterface component)
	{
		assertEquals(ITestService.RESULT, component.getInjectedField().getString());
		assertNull(component.getInjectedOptionalField());
		assertEquals(ITestService.RESULT_RED, component.getInjectedFieldRed().getString());
		assertEquals(ITestService.RESULT_BLUE, component.getInjectedFieldBlue().getString());

		assertEquals(ITestService.RESULT, component.getInjectedFieldProvider().get().getString());

		assertEquals(ITestService.RESULT,
			component.getInjectedTypeLiteralField().get(ITestService.RESULT));

		assertEquals("NAMED_1", component.getNamed1());
		assertEquals("NAMED_2", component.getNamed2());
		assertEquals("evil123", component.getEvilId());
	}

	/**
	 * Helper class to make binding of an instance of jakarta.inject.Named less verbose
	 */
	private static class Jsr330Named implements Named
	{
		private final String name;

		private Jsr330Named(String name)
		{
			this.name = name;
		}

		@Override
		public String value()
		{
			return name;
		}

		@Override
		public Class<? extends Annotation> annotationType()
		{
			return Named.class;
		}
	}
}
