// ****************************************************************
// This is free software licensed under the NUnit license. You
// may obtain a copy of the license as well as information regarding
// copyright ownership at http://nunit.org.
// ****************************************************************

using System;
using System.ComponentModel;
using System.IO;
using System.Collections;
using System.Reflection;
using NUnit.Core.Extensibility;

namespace NUnit.Core.Builders
{
    /// <summary>
    /// Class that builds a TestSuite from an assembly
    /// </summary>
    public class TestAssemblyBuilder
    {
        static Logger log = InternalTrace.GetLogger("TestAssemblyBuilder");

        #region Instance Fields
        /// <summary>
        /// The loaded assembly
        /// </summary>
        Assembly assembly;

        /// <summary>
        /// Our LegacySuite builder, which is only used when a 
        /// fixture has been passed by name on the command line.
        /// </summary>
        ISuiteBuilder legacySuiteBuilder;

        private TestAssemblyInfo assemblyInfo = null;

        #endregion

        #region Properties
        public Assembly Assembly
        {
            get { return assembly; }
        }

        public TestAssemblyInfo AssemblyInfo
        {
            get
            {
                if(assemblyInfo == null && assembly != null)
                {
                    using(AssemblyReader rdr = new AssemblyReader(assembly))
                    {
                        Version imageRuntimeVersion = new Version(rdr.ImageRuntimeVersion.Substring(1));
                        IList frameworks = CoreExtensions.Host.TestFrameworks.GetReferencedFrameworks(assembly);
                        assemblyInfo = new TestAssemblyInfo(rdr.AssemblyPath, imageRuntimeVersion, RuntimeFramework.CurrentFramework, frameworks);
                    }
                }

                return assemblyInfo;
            }
        }
        #endregion

        #region Constructor

        public TestAssemblyBuilder()
        {
            // TODO: Keeping this separate till we can make
            //it work in all situations.
            legacySuiteBuilder = new NUnit.Core.Builders.LegacySuiteBuilder();
        }

        #endregion

        #region Build Methods
        public Test Build(string assemblyName, string testName, bool autoSuites)
        {
            if(testName == null || testName == string.Empty)
                return Build(assemblyName, autoSuites);

            // Change currentDirectory in case assembly references unmanaged dlls
            // and so that any addins are able to access the directory easily.
            using(new DirectorySwapper(Path.GetDirectoryName(assemblyName)))
            {
                this.assembly = Load(assemblyName);
                if(assembly == null) return null;

                // If provided test name is actually the name of
                // a type, we handle it specially
                Type testType = assembly.GetType(testName);
                if(testType != null)
                    return Build(assembly, assemblyName, testType, autoSuites);

                // Assume that testName is a namespace and get all fixtures in it
                IList fixtures = GetFixtures(assembly, testName);
                if(fixtures.Count > 0)
                    return BuildTestAssembly(this.assembly, assemblyName, fixtures, autoSuites);

                return null;
            }
        }

        public TestSuite Build(string assemblyName, bool autoSuites)
        {
            // Change currentDirectory in case assembly references unmanaged dlls
            // and so that any addins are able to access the directory easily.
            using(new DirectorySwapper(Path.GetDirectoryName(assemblyName)))
            {
                this.assembly = Load(assemblyName);
                if(this.assembly == null) return null;

                IList fixtures = GetFixtures(assembly, null);
                return BuildTestAssembly(this.assembly, assemblyName, fixtures, autoSuites);
            }
        }

        private Test Build(Assembly assembly, string assemblyName, Type testType, bool autoSuites)
        {
            // TODO: This is the only situation in which we currently
            // recognize and load legacy suites. We need to determine 
            // whether to allow them in more places.
            if(legacySuiteBuilder.CanBuildFrom(testType))
                return legacySuiteBuilder.BuildFrom(testType);
            else if(TestFixtureBuilder.CanBuildFrom(testType))
                return BuildTestAssembly(assembly, assemblyName,
                    new Test[] { TestFixtureBuilder.BuildFrom(testType) }, autoSuites);
            return null;
        }

        private TestSuite BuildTestAssembly(Assembly assembly, string assemblyName, IList fixtures, bool autoSuites)
        {
            TestSuite testAssembly = new TestAssembly(assembly, assemblyName);

            if(autoSuites)
            {
                NamespaceTreeBuilder treeBuilder =
                    new NamespaceTreeBuilder(testAssembly);
                treeBuilder.Add(fixtures);
                testAssembly = treeBuilder.RootSuite;
            }
            else
                foreach(TestSuite fixture in fixtures)
                {
                    if(fixture != null)
                    {
                        if(fixture is SetUpFixture)
                        {
                            fixture.RunState = RunState.NotRunnable;
                            fixture.IgnoreReason = "SetUpFixture cannot be used when loading tests as a flat list of fixtures";
                        }

                        testAssembly.Add(fixture);
                    }
                }

            NUnitFramework.ApplyCommonAttributes(assembly, testAssembly);

            testAssembly.Properties["_PID"] = System.Diagnostics.Process.GetCurrentProcess().Id;
            testAssembly.Properties["_APPDOMAIN"] = AppDomain.CurrentDomain.FriendlyName;


            // TODO: Make this an option? Add Option to sort assemblies as well?
            testAssembly.Sort();

            return testAssembly;
        }

        #endregion

        #region Helper Methods

        private Assembly Load(string path)
        {
            Assembly assembly = null;

            // Throws if this isn't a managed assembly or if it was built
            // with a later version of the same assembly. 
            AssemblyName assemblyName = AssemblyName.GetAssemblyName(Path.GetFileName(path));

            assembly = Assembly.Load(assemblyName);

            if(assembly != null)
                CoreExtensions.Host.InstallAdhocExtensions(assembly);

            log.Info("Loaded assembly " + assembly.FullName);

            return assembly;
        }

        private IList GetFixtures(Assembly assembly, string ns)
        {
            ArrayList fixtures = new ArrayList();
            log.Debug("Examining assembly for test fixtures");

            IList testTypes = GetCandidateFixtureTypes(assembly, ns);

            log.Debug("Found {0} classes to examine", testTypes.Count);
#if CLR_2_0 || CLR_4_0
            System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();
            timer.Start();
#endif

            foreach(Type testType in testTypes)
            {
                if(TestFixtureBuilder.CanBuildFrom(testType))
                    fixtures.Add(TestFixtureBuilder.BuildFrom(testType));
            }

#if CLR_2_0 || CLR_4_0
            log.Debug("Found {0} fixtures in {1} seconds", fixtures.Count, timer.Elapsed);
#else
            log.Debug("Found {0} fixtures", fixtures.Count);
#endif

            return fixtures;
        }

        private IList GetCandidateFixtureTypes(Assembly assembly, string ns)
        {
            // Skip if the assembly references NUnit 3+
            foreach(AssemblyName an in assembly.GetReferencedAssemblies())
            {
                if((an.Name == "nunit.framework") && an.Version.Major > 2)
                {
                    string msg = string.Format("Skipped loading assembly {0} because it references an unsupported version of the nunit.framework, {1}",
                        assembly.GetName().Name, an.Version);
                    log.Warning(msg);
                    throw new UnsupportedFrameworkException(msg);
                }
            }

            IList types = assembly.GetTypes();

            if(ns == null || ns == string.Empty || types.Count == 0)
                return types;

            string prefix = ns + ".";

            ArrayList result = new ArrayList();
            foreach(Type type in types)
                if(type.FullName.StartsWith(prefix))
                    result.Add(type);

            return result;
        }
        #endregion
    }
}
