/*
 * 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.brooklyn.rest.resources;

import static org.testng.Assert.assertEquals;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.ws.rs.core.MediaType;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.entity.stock.BasicEntity;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.EntitySpec;
import org.apache.brooklyn.rest.domain.TaskSummary;
import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.http.HttpAsserts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import javax.ws.rs.core.Response;

@Test(singleThreaded = true,
        // by using a different suite name we disallow interleaving other tests between the methods of this test class, which wrecks the test fixtures
        suiteName = "EntityResourceTest")
public class EntityResourceTest extends BrooklynRestResourceTest {

    private static final Logger log = LoggerFactory.getLogger(EntityResourceTest.class);
    
    private final ApplicationSpec simpleSpec = ApplicationSpec.builder()
            .name("simple-app")
            .entities(ImmutableSet.of(new EntitySpec("simple-ent", RestMockSimpleEntity.class.getName())))
            .locations(ImmutableSet.of("localhost"))
            .build();

    private EntityInternal entity;

    private static final String entityEndpoint = "/applications/simple-app/entities/simple-ent";

    @BeforeClass(alwaysRun = true)
    public void setUp() throws Exception {
        // Deploy application
        startServer();
        Response deploy = clientDeploy(simpleSpec);
        waitForApplicationToBeRunning(deploy.getLocation());

        // Add tag
        entity = (EntityInternal) Iterables.find(getManagementContext().getEntityManager().getEntities(), new Predicate<Entity>() {
            @Override
            public boolean apply(@Nullable Entity input) {
                return "RestMockSimpleEntity".equals(input.getEntityType().getSimpleName());
            }
        });
    }

    @Test
    public void testGetSpecAcceptingXYaml() throws Exception {
        testGetSpec("text/x-yaml");
    }
    
    @Test
    public void testGetSpecWithNoExplicitAccepts() throws Exception {
        testGetSpec(null);
    }
    
    protected void testGetSpec(String acceptMimeType) throws Exception {
        String appName = "ent-with-spec";
        
        // Create an app with a yaml spec
        String yaml = Joiner.on("\n").join(
                "name: " + appName,
                "services:",
                "- type: "+BasicEntity.class.getName());
        Response appResponse = client().path("/applications")
                .header("Content-Type", "text/x-yaml")
                .post(yaml);
        waitForApplicationToBeRunning(appResponse.getLocation());

        // Retrieve the yaml spec, and confirm it is as expected (not wrapped in quotes, and treating \n sensibly)
        Response response;
        if (acceptMimeType != null) {
            response = client().path("/applications/" + appName + "/entities/" + appName + "/spec")
                    .accept(acceptMimeType)
                    .get();
        } else {
            response = client().path("/applications/" + appName + "/entities/" + appName + "/spec")
                    .get();
        }
        String data = response.readEntity(String.class);

        assertEquals(data.trim(), yaml.trim());
    }
    
    @Test
    public void testTagsSanity() throws Exception {
        entity.tags().addTag("foo");
        
        Response response = client().path(entityEndpoint + "/tags")
                .accept(MediaType.APPLICATION_JSON_TYPE)
                .get();
        String data = response.readEntity(String.class);
        
        try {
            List<Object> tags = new ObjectMapper().readValue(data, new TypeReference<List<Object>>() {});
            Assert.assertTrue(tags.contains("foo"));
            Assert.assertFalse(tags.contains("bar"));
        } catch (Exception e) {
            Exceptions.propagateIfFatal(e);
            throw new IllegalStateException("Error with deserialization of tags list: "+e+"\n"+data, e);
        }
    }
    
    @Test
    public void testRename() throws Exception {
        try {
            Response response = client().path(entityEndpoint + "/name")
                .query("name", "New Name")
                .post(null);

            HttpAsserts.assertHealthyStatusCode(response.getStatus());
            Assert.assertTrue(entity.getDisplayName().equals("New Name"));
        } finally {
            // restore it for other tests!
            entity.setDisplayName("simple-ent");
        }
    }
    
    @Test
    public void testAddChild() throws Exception {
        try {
            // to test in GUI: 
            // services: [ { type: org.apache.brooklyn.entity.stock.BasicEntity }]
            Response response = client().path(entityEndpoint + "/children").query("timeout", "10s")
                .post(javax.ws.rs.client.Entity.entity("services: [ { type: "+TestEntity.class.getName()+" }]", "application/yaml"));

            HttpAsserts.assertHealthyStatusCode(response.getStatus());
            Assert.assertEquals(entity.getChildren().size(), 1);
            Entity child = Iterables.getOnlyElement(entity.getChildren());
            Assert.assertTrue(Entities.isManaged(child));
            
            TaskSummary task = response.readEntity(TaskSummary.class);
            Assert.assertEquals(task.getResult(), MutableList.of(child.getId()));
            
        } finally {
            // restore it for other tests
            Collection<Entity> children = entity.getChildren();
            if (!children.isEmpty()) Entities.unmanage(Iterables.getOnlyElement(children));
        }
    }
    
    @Test
    public void testTagsDoNotSerializeTooMuch() throws Exception {
        entity.tags().addTag("foo");
        entity.tags().addTag(entity.getParent());

        Response response = client().path(entityEndpoint + "/tags")
                .accept(MediaType.APPLICATION_JSON)
                .get();
        String raw = response.readEntity(String.class);
        log.info("TAGS raw: "+raw);
        HttpAsserts.assertHealthyStatusCode(response.getStatus());
        
        Assert.assertTrue(raw.contains(entity.getParent().getId()), "unexpected app tag, does not include ID: "+raw);
        
        Assert.assertTrue(raw.length() < 1000, "unexpected app tag, includes too much mgmt info (len "+raw.length()+"): "+raw);
        
        Assert.assertFalse(raw.contains(entity.getManagementContext().getManagementNodeId()), "unexpected app tag, includes too much mgmt info: "+raw);
        Assert.assertFalse(raw.contains("managementContext"), "unexpected app tag, includes too much mgmt info: "+raw);
        Assert.assertFalse(raw.contains("localhost"), "unexpected app tag, includes too much mgmt info: "+raw);
        Assert.assertFalse(raw.contains("catalog"), "unexpected app tag, includes too much mgmt info: "+raw);

        @SuppressWarnings("unchecked")
        List<Object> tags = mapper().readValue(raw, List.class);
        log.info("TAGS are: "+tags);
        
        Assert.assertEquals(tags.size(), 2, "tags are: "+tags);

        Assert.assertTrue(tags.contains("foo"));
        Assert.assertFalse(tags.contains("bar"));
        
        MutableList<Object> appTags = MutableList.copyOf(tags);
        appTags.remove("foo");
        Object appTag = Iterables.getOnlyElement( appTags );
        
        // it's a map at this point, because there was no way to make it something stronger than Object
        Assert.assertTrue(appTag instanceof Map, "Should have deserialized an entity: "+appTag);
        // let's re-serialize it as an entity
        appTag = mapper().readValue(mapper().writeValueAsString(appTag), Entity.class);
        
        Assert.assertTrue(appTag instanceof Entity, "Should have deserialized an entity: "+appTag);
        Assert.assertEquals( ((Entity)appTag).getId(), entity.getApplicationId(), "Wrong ID: "+appTag);
        Assert.assertTrue(appTag instanceof BasicApplication, "Should have deserialized BasicApplication: "+appTag);
    }
    
}
